Socket 網絡編程框架
Socket(套接字)是一個網絡編程概念,描述了一個通信端點(Endpoint),用于建立網絡連接(Connection)并傳輸數據。
Linux Kernel 提供了一套面向 Socket 的網絡編程框架,并通過提供一組標準的 System call APIs,使得開發者可以在 Userspace 中便捷的開發各種 Network Applications,例如:基于 HTTP 協議的 Web 服務器、基于 SMTP 協議的郵件服務器、基于 FTP 協議的文件服務器等等。
Linux Socket 網絡編程框架主要由 3 大模塊組成:
- BSD Socket APIs
- Socket Abstraction Layer
- VFS Layer
BSD Socket APIs 概覽
BSD Socket APIs(Berkeley Software Distribution Socket APIs),是面向 Userspace Application 的接口封裝層,提供了一套兼容絕大部分網絡通信協議族的標準 Socket APIs。
- socket():創建一個新的 socket,返回一個 int 類型的 socket fd(File Descriptor,套接字文件描述符),用于后續的網絡連接操作。
- bind():將 socket 與一個本地 IP:Port 綁定,通常用于服務端,以便在本地監聽網絡連接。
- connect():建立與遠程主機的連接,通常用于客戶端,以便連接到遠程服務器。
- listen():開始監聽來自遠程主機的連接請求,通常用于服務器端,等待來自客戶端的連接請求。
- accept():接受一個連接請求,返回一個新的 socket fd,通常用于服務器端,用于接收客戶端的連接請求。
- send():向 socket 發送數據。
- recv():從 socket 接收數據。
- close():關閉 socket 連接。
Socket API 的使用通常可以分為以下幾個步驟:
- 創建套接字:使用 socket() 函數創建一個新的 socket fd。
- 配置套接字:使用一些其他的 Socket API 函數,例如 bind()、connect() 和 listen() 來配置 socket,使其能夠接收和發送數據。
- 數據傳輸:使用 send() 和 recv() 函數進行數據傳輸。
- 關閉套接字:使用 close() 函數關閉 socket 連接。
需要注意的是,Socket API 并不是線程安全的,如果有多個線程同時使用了同一個 socket fd,則可能會導致數據傳輸錯誤或其他問題。為了避免這種情況,Application 需要進行適當的同步和數據處理。
Socket Abstraction Layer
Socket Abstraction Layer(Socket 抽象層),是 Socket API 的底層支撐,主要負責以下工作:
- 實現了 Socket File System(套接字文件系統),用于管理 User Process 和 socket fd 之間的關系,包括 socket fd 的創建、打開、讀寫等操作。
- 實現了 Struct Socket、Struct Sock、Protocol Family(協議族)、Address Family(地址族)等數據結構。
- 實現了 TCP/IP 協議棧,包括:TCP、UDP、ICMP 等協議。
- 實現了 L4 傳輸層功能,處理傳輸層協議的連接建立、數據傳輸、連接維護等操作。
Socket & Sock
- Struct Socket 是在 Socket Layer 中定義的數據結構,面向上層 Socket API,包含了一個 Socket 所具有的各種屬性,例如:狀態、類型、標記、關聯的 Sock 等。
- Struct Sock 是在 Sock Layer 中定義的數據結構,面向底層協議棧實現,表示一個 Socket 對應的 PCB(Protocol Control Block,協議控制塊),即:與某種網絡協議相關的一些信息和狀態,例如:TCP PCB 就包括了 TCP 連接狀態、發送緩沖區、接收緩沖區、擁塞窗口等。
Socket Layer 與 Network Driver(網絡設備驅動程序)之間通過 Socket Buffer(skb_buff)進行交互,當 Socket Layer 接收到 Application 的數據時,會將數據存儲在 Socket Buffer 中,并將 Socket Buffer 傳遞給對應的 Sock Layer 進行處理。Struct Socket 和 Struct Sock 之間通過指針進行關聯綁定,共同實現 Socket API 的功能。
Socket Layer
// linux/include/linux/net.h
/**
- struct socket - general BSD socket
- @state: socket state (%SS_CONNECTED, etc)
- @type: socket type (%SOCK_STREAM, etc)
- @flags: socket flags (%SOCK_NOSPACE, etc)
- @ops: protocol specific socket operations
- @file: File back pointer for gc
- @sk: internal networking protocol agnostic socket representation
- @wq: wait queue for several uses
*/
struct socket {
socket_state state;
short type; // 套接字類型,如 SOCK_STREAM、SOCK_DGRAM 等;
unsigned long flags; // 套接字標志,如 O_NONBLOCK、O_ASYNC 等;
struct file *file; // 套接字對應的文件結構體;
struct sock *sk; // 指向套接字對應的 Sock 結構體;
const struct proto_ops *ops; // 套接字對應的操作函數集,如 inet_stream_ops、inet_dgram_ops 等;
struct socket_wq wq; // 套接字等待隊列;
};
typedef enum
{
SS_FREE=0; // 未分配
SS_UNCONNECTED; // 未連接到任何套接字
SS_CONNECTING; // 處于連接過程中
SS_CONNECTED; // 已經連接到另一個套接字
SS_DISCONNECTING; // 處于斷開連接過程中
} socket_state;
Sock Layer
Struct Sock 包含了 Socket 的各種底層執行狀態和操作信息,例如:接收和發送緩沖區、套接字隊列、套接字協議信息等。
// linux/include/net/sock.h
struct sock {
/* Socket family and type */
unsigned short family; // 協議族,如 AF_INET、AF_PACKET 等;
__u16 type; // 套接字類型,如 SOCK_STREAM、SOCK_DGRAM 等;
unsigned long flags; // 套接字標志,如 O_NONBLOCK、O_ASYNC 等;
/* Protocol specific elements of the socket */
struct proto *ops; // 協議特定操作函數集;
struct net_device *sk_net; // 套接字所在的網絡設備;
/* Memory allocation cache */
kmem_cache_t *sk_slab; // 套接字內存分配緩存;
/* Socket state */
atomic_t refcnt; // 套接字引用計數;
struct mutex sk_lock; // 套接字鎖,用于保護套接字狀態的一致性;
/* Send and receive buffers */
struct sk_buff_head sk_receive_queue; // 接收隊列,保存了等待接收的數據包;
struct sk_buff_head sk_write_queue; // 發送隊列,保存了等待發送的數據包;
struct sk_buff *sk_send_head; // 發送緩沖區的頭指針;
struct sk_buff *sk_send_tail; // 發送緩沖區的尾指針;
/* Receive queue */
struct sk_buff *sk_receive_skb; // 當前正在接收的數據包;
/* Transport specific fields */
__u32 sk_priority; // 套接字優先級;
struct dst_entry *sk_dst_cache; // 緩存的目標設備;
struct dst_entry *sk_dst_pending_confirm;
struct flowi sk_fl; // Flowi 結構體,保存了套接字相關的流信息;
struct sk_filter *sk_filter; // 過濾器;
struct sk_buff_head sk_async_wait_queue; // 異步等待隊列;
/* Socket buffer allocations */
unsigned long sk_wmem_alloc; // 發送緩沖區已分配的內存;
unsigned long sk_omem_alloc;
/* User and kernel buffers */
struct socket_wq *sk_wq; // 套接字等待隊列;
struct page_frag sk_frag; // 內存分配器的頁片段;
int sk_forward_alloc; // 前向分配的字節數;
int sk_rxhash; // 套接字是否支持接收哈希。
};
Protocol Family
Socket 支持廣泛的 PFs,主要有以下 4 類:
- PF_INETv4v6 sockets(IP Socket):基于 IPv4v6 網絡層協議,支持 TCP、UDP 傳輸層協議。
- SOCK_STREAM:TCP 字節流式傳輸。
- SOCK_DGRAM:UDP 數據包式傳輸。
- SOCK_RAW:原始套接字,可以處理 IPv4、ICMP、IGMP 等報文,常用于網絡監聽、檢驗新的協議或者訪問新的設備。
- PF_PACKET sockets(Packet Socket):基于 Device Driver(設備驅動),支持對底層數據包的捕獲和注入,常用于網絡安全、網絡監測等場景,例如:網絡嗅探、協議分析、數據包過濾等。
- PF_NETLINK sockets(Netlink Socket):支持 Kernel Space 和 User Space 之間的通信,常用于網絡管理和網絡監測等場景,例如:獲取內核中的網絡信息、配置內核的網絡參數、監控網絡狀態等。
- PF_UNIX sockets(UNIX socket):用于 Unix-like 系統中的多進程之間通信。
值得注意的是,雖然不同的協議族都使用了同一套 Socket API,但也可能會存在一些特有的函數或者數據結構,用于實現協議族特有的功能。例如:
- PF_PACKET 協議族可以使用 pcap 庫來進行網絡數據包捕獲和注入;
- PF_NETLINK 協議族可以使用 netlink 庫來進行內核和用戶空間之間的通信。
但是,這些特有的函數和數據結構通常不會影響套接字編程接口的基本使用方式和語法。
VFS Layer
VFS Layer 屬于 Linux VFS sub-system(虛擬文件子系統),提供了一組通用的 Linux File System Call APIs(SCI),使得 Application 可以使用相同的 API 來完成文件 I/O。
當 Application 使用 Socket API 發送或接收數據時,Socket Abstraction Layer 會借助 VFS Layer 來完成 Socket File System 的管理。例如:
- Application 調用 Socket API socket() 創建 socket 時:在 VFS I/O Layer 中,Socket FD 文件句柄被創建。
- Application 調用 Socket API close() 關閉 socket 時:在 VFS I/O Layer 中,文件句柄被釋放,并釋放相關資源。
PF_INET sockets
PF_INET sockets 基于 IPv4v6 網絡層協議,支持 TCP、UDP 等傳輸層協議。是 Linux 網絡編程中最常用到的協議族。
1、創建套接字
socket()
函數功能:創建一個新的套接字,返回一個 int 類型的套接字文件描述符(作為 Linux 文件操作的句柄),用于后續的網絡連接操作。
函數原型:
- af 參數:指定 Socket AF(Address Family,地址族),對于 PF_INETv4v6 sockets 而言,可選:
- AF_INET
- AF_INET6
- type 參數:指定數據傳輸方式,可選:
- SOCK_STREAM(面向連接的 TCP)
- SOCK_DGRAM(無連接的 UDP)
- SOCK_RAW(原始 IP 數據包)
- protocol:指定具體的傳輸層協議,可選:
- IPPROTO_TCP
- IPPTOTO_UDP
- 函數返回值:
- 成功:返回 Socket fd。
- 失?。悍祷?-1。
#include
int socket(int af, int type, int protocol);
示例:
// 創建 TCP 套接字
int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 創建 UDP 套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
setsockopt()
函數功能:用于設置 Socket 的選項值。
函數原型:
- sockfd 參數:指定 socket fd。
- level 參數:指定選項的協議層,可選 SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP 等。
- optname 參數:指定要設置的選項名。
- SO_REUSEADDR:int 類型,表示重用 IP 地址。
- SO_KEEPALIVE:int 類型,用于啟用/禁用 Keepalive(保持連接)功能。
- SO_LINGER:struct linger 類型,用于指定關閉套接字時的行為。
- TCP_NODELAY:int 類型,用于禁用 Nagle 算法,從而實現數據的實時傳輸。
- optval 參數:指定存放選項值的緩沖區入口。
- optlen 參數:指定選項值緩沖區的長度。
- 函數返回值:
- 成功:0。
- 失敗:-1,并設置了 errno 錯誤碼。
#include
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
2、配置套接字
bind()
將 Socket 與主機中的某個 IP:Port 綁定起來。
函數作用:將套接字與一個本地 IP:Port 綁定。通常用于服務端,以便在本地監聽網絡連接。函數原型:
- sock 參數:指定 Server socket 文件描述符。
- addr 參數:指定 Server sockaddr 結構體變量,指針類型。
- addrlen 參數:指定 addr 變量的大小,使用 sizeof() 計算得出。
- 函數返回值:
- 失敗:返回 -1。
#include
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);
示例:
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in tcp_socket_addr; // 定義 Server Socket Address
memset(&tcp_socket_addr, 0, sizeof(tcp_socket_addr)); // 初始化結構體內存
tcp_socket_addr.sin_family = PF_INET;
tcp_socket_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 定義本地 IP 地址
tcp_socket_addr.sin_port = htons(1314); // 定義本地 Port
bind(tcp_socket, (sockaddr *)&tcp_socket_addr, sizeof(sockaddr)); // 綁定
其中 sockaddr_in 結構類型的聲明如下。使用時,需要先定義并初始化 sockaddr_in,然后再將它強制轉化成 sockaddr 來使用。2 個結構體長度均為 16B,其中,sockaddr_in.sin_family 的 2B 存入 sockaddr.sa_family,剩下的 14B 存入 sockaddr.sa_data。
這樣做是為了在后續的各種操作中可以更方便的處理 IP 地址和 Port 號。
#include
struct in_addr {
unsigned long a_addr;
}
struct sockaddr_in {
unsigned short sin_family; // 地址類型(2B)
unsigned short int sin_port; // 端口號(2B)
struct in_addr sin_addr; // IP 地址(4B)
unsigned char sin_zero[8]; // 填充空間(8B)
}
struct sockaddr {
unsigned short sa_family; // 地址類型(2B)
char sa_data[14]; // 協議地址(14B)
}
另外,IPv6 的結構體聲明如下:
struct sockaddr_in6
{
sa_family_t sin6_family; // 地址類型,取值為 AF_INET6
in_port_t sin6_port; // 16 位端口號
uint32_t sin6_flowinfo; // IPv6 流信息
struct in6_addr sin6_addr; // 具體的 IPv6 地址
uint32_t sin6_scope_id; // 接口范圍 ID
};
如果 sock_addr.sin_port 賦值為 0,或者沒有調用 bind(),而直接調用 listen(),那么 Kernel 會自動為 Socket 臨時分配一個 Port。此時需要調用 getsockname() 來獲取具體的端口信息。
getsockname(httpd, (struct sockaddr *)&name, &namelen);
ntohs(name.sin_port);
listen()
函數作用:開始監聽來自遠程主機的連接請求。通常用于服務器端,在套接字上等待來自客戶端的連接請求。
函數原型:
- sock 參數:指定需要進入監聽狀態的 Server socket 文件描述符。
- backlog 參數:指定請求隊列的最大長度,當隊列滿了之后,就不再接收請求。
- 函數返回值:
- 失?。悍祷?-1。
#include
int listen(int sock, int backlog);
connect()
函數作用:建立與遠程主機的連接。通常用于客戶端,以便連接到遠程服務器。函數原型:
- sock 參數:指定 Client socket 文件描述符。
- serv_addr 參數:指定 Server sockaddr 結構體變量,指針類型。
- addrlen 參數:指定 addr 變量的大小,使用 sizeof() 計算得出。
- 函數返回值:
- 失?。悍祷?-1。
#include
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);
示例:
int cli_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in server_sock_addr; // 定義 Server Socket Address
memset(&server_sock_addr, 0, sizeof(server_sock_addr)); // 初始化結構體內存
server_sock_addr.sin_family = PF_INET;
server_sock_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 定義本地 IP 地址
server_sock_addr.sin_port = htons(1314); // 定義本地 Port
connect(cli_socket, (sockaddr *)&server_sock_addr, sizeof(sockaddr));
accept()
函數作用:接受一個連接請求,返回一個新的、表示客戶端的 Socket 文件描述符,作為服務端和客戶端之間發送與接收操作的句柄。通常用于服務器端,用于接收客戶端的連接請求。
函數原型:
- sock 參數:指定 Server socket 文件描述符。
- addr 參數:指定 Client sockaddr 結構體變量,指針類型。
- addrlen 參數:指定 addr 變量的大小,使用 sizeof() 計算得出。
- 函數返回值:
- 成功:返回 Client socket fd。
- 失?。悍祷?-1。
#include
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
示例:
// 返回一個新的套接字,用于后續的發送和接收
int cli_socket = accept(server_socket, (sockeraddr *)&cli_socket_addr, &len);
getnameinfo()
函數作用:用于將一個 Sock Addr 轉換為對應的 Hostname 或 Service name,以便于記錄日志或者顯示給用戶。
函數原型:
- addr:表示需要轉換的 Sock Addr;
- addrlen:表示該 Socket addr址的長度;
- host:輸出 Hostname 的存儲空間。
- serv:輸出 Service name 的存儲空間。
- hostlen:Hostname 存儲空間的大小。
- servlen:Service name 存儲空間的大小。
- flags:標志參數,通常設置為 0。
- 函數返回值:
- 成功:返回 0。
- 失?。悍祷胤?0,并更新 errno 全局變量。
#include
int getnameinfo(const struct sockaddr *addr, socklen_t addrlen,
char *host, socklen_t hostlen,
char *serv, socklen_t servlen, int flags);
3、數據傳輸
recv() 和 send()
recv() 和 send() 函數,用于在 TCP Socket 中進行數據讀寫,屬于阻塞式 I/O(Blocking I/O)模式,即:如果沒有可讀數據或者對端的接收緩沖區已滿,則函數將一直等待直到有數據可讀或者對端緩沖區可寫。
recv():從套接字接收數據。
- sockfd 參數:指定要接收 TCP 數據的 Socket 文件描述符。
- buf 參數:指定接收數據緩沖區的入口地址。
- len 參數:指定要接收的數據的 Byte 數目。
- flags:指定接收數據時的選項,常設為 0。
- 函數返回值:
- 成功:返回接收的字節數。
- 失敗:返回 -1。
#include
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
send():向套接字發送數據。
- sockfd 參數:指定要發送 TCP 數據的 Socket 文件描述符。
- buf 參數:指定發送數據緩沖區入的口地址。
- len 參數:指定要發送數據的 Byte 數目。
- flags 參數:指定發送數據時的選項,常設為 0。
- 函數返回值:
- 成功:返回發送的字節數。
- 失?。悍祷?-1。
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
recvfrom() 和 sendto()
recvfrom() 和 sendto() 函數,用于在 UDP Socket 中進行數據讀寫以及獲取對端地址。這兩個函數在使用時需要指定對端的 IP:Port。
recvfrom():
- sock 參數:指定要接收 UDP 數據的 Socket 文件描述符。
- buf 參數:指定接收數據緩沖區的入口地址。
- nbytes 參數:指定要接收數據的 Byte 數目。
- flags 參數:指定接收數據時的選項,常設為 0。
- from 參數:指定源地址 sockaddr 結構體變量的地址。
- addrlen 參數:指定 from 參數使用的長度,使用 sizeof() 獲取。
- 函數返回值:
- 成功:返回接收的字節數。
- 失敗:返回 -1。
#include
ssize_t recvfrom(int sock, void *buf, size_t nbytes, int flags, struct sockadr *from, socklen_t *addrlen);
sendto():
- sock 參數:指定要發送 UDP 數據的 Socket 文件描述符。
- buf 參數:指定發送數據緩沖區的入口地址。
- nbytes 參數:指定要發送數據的 Byte 數目。
- flags 參數:指定發送數據時的選項,常設為 0。
- to 參數:指定目標地址 sockaddr 結構體變量的地址。
- addrlen 參數:指定 to 參數使用的長度,使用 sizeof() 獲取。
- 函數返回值:
- 成功:返回發送的字節數。
- 失敗:返回 -1。
#include
ssize_t sendto(int sock, void *buf, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);
recvmsg() 和 sendmsg()
recvmsg() 和 sendmsg() 函數,用于在 TCP 和 UDP Socket 中進行數據讀寫,不僅可以讀寫數據,還可以讀寫對端地址、輔助數據等信息。
recvmsg():
- sock 參數:指定要接收 TCP 或 UDP 數據的 Socket 文件描述符。
- msg 參數:指示將接收的數據存儲到 msghdr 結構體中。
- flags 參數:支持函數的行為,可選 0 或者 MSG_DONTWAIT 等標志位。
- 函數返回值:
- 成功:返回接收的字節數。
- 失?。悍祷?-1。
#include
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
sendmsg()
- sock 參數:指定要發送 TCP 或 UDP 數據的 Socket 文件描述符。
- msg 參數:指示 msghdr 結構體中包含了要發送的數據、數據長度等信息。
- flags 參數:支持函數的行為,可選 0 或者 MSG_DONTWAIT 等標志位。
- 函數返回值:
- 成功:返回發送的字節數。
- 失敗:返回 -1。
#include
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
msghdr 結構體定義如下:
struct msghdr {
/* 指定接收或發送數據的對端地址,可以為 NULL 或 0,表示不需要使用對端地址。*/
void msg_name; / optional address /
socklen_t msg_namelen; / size of address */
/* 指定接收或發送數據的緩沖區和緩沖區大小,可以使用多個緩沖區同時接收或發送數據。*/
struct iovec *msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
/* 指定一些附加的控制信息,可以為 NULL 或 0。*/
void msg_control; / ancillary data, see below /
size_t msg_controllen; / ancillary data buffer len */
/* 指定函數的行為,例如是否需要接收帶外數據等。/
int msg_flags; / flags on received message */
};
flags 參數類型
- MSG_PEEK:允許從接收隊列中查看數據而不將其刪除。這意味著,如果接收隊列中有數據,recv() 函數將返回數據的一個副本,但是該數據仍將留在接收隊列中。這對于查看接收隊列中的數據而不實際處理它們非常有用。此外,使用 MSG_PEEK 選項,我們可以檢查套接字緩沖區中是否有足夠的數據可供讀取,以便稍后調用 recv() 函數。
- MSG_WAITALL:如果套接字緩沖區中沒有足夠的數據,則 recv() 函數將一直等待,直到收到請求的數據量。
- MSG_DONTWAIT:指定此標志后,recv() 函數將立即返回,即使沒有收到數據也不會阻塞。如果沒有數據可用,則 recv() 將返回 -1,并將 errno 設置為 EAGAIN 或 EWOULDBLOCK。
- MSG_OOB:用于處理帶外數據,即緊急數據。帶外數據不遵循正常的傳輸控制協議(如 TCP),可以使用此標志將其標記為緊急數據并將其與其他數據分開處理。
- MSG_TRUNC:如果接收緩沖區中的數據比接收緩沖區長度長,則截斷數據并返回。
- MSG_CTRUNC:如果接收緩沖區中的控制消息(例如帶外數據或錯誤消息)比接收緩沖區長度長,則截斷消息并返回。
4、關閉套接字
close()
函數作用:關閉套接字連接。函數原型:
- fd:指定要關閉的 Socket 的文件描述符。
- 函數返回值:
- 失敗:返回 -1。
#include
int close(int fd);
TCP Socket 編程示例
服務端
#include
#include
#include
#include
#include
#include
#include
#define ERR_MSG(err_code) do {
err_code = errno;
fprintf(stderr, "ERROR code: %d n", err_code);
perror("PERROR message");
} while (0)
const int BUF_LEN = 100;
int main(void)
{
/* 配置 Server Sock 信息。*/
struct sockaddr_in srv_sock_addr;
memset(&srv_sock_addr, 0, sizeof(srv_sock_addr));
srv_sock_addr.sin_family = AF_INET;
srv_sock_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 即 0.0.0.0 表示監聽本機所有的 IP 地址。
srv_sock_addr.sin_port = htons(6666);
/* 創建 Server Socket。*/
int srv_socket_fd = 0;
if (-1 == (srv_socket_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP))) {
printf("Create socket file descriptor ERROR.n");
ERR_MSG(errno);
exit(EXIT_FAILURE);
}
/* 設置 Server Socket 選項。*/
int optval = 1;
if (setsockopt(srv_socket_fd,
SOL_SOCKET, // 表示套接字選項的協議層。
SO_REUSEADDR, // 表示在綁定地址時允許重用本地地址。這樣做的好處是,當服務器進程崩潰或被關閉時,可以更快地重新啟動服務器,而不必等待一段時間來釋放之前使用的套接字。
&optval,
sizeof(optval)) < 0)
{
printf("Set socket options ERROR.n");
ERR_MSG(errno);
exit(EXIT_FAILURE);
}
/* 綁定 Socket 與 Sock Address 信息。*/
if (-1 == bind(srv_socket_fd,
(struct sockaddr *)&srv_sock_addr,
sizeof(srv_sock_addr)))
{
printf("Bind socket ERROR.n");
ERR_MSG(errno);
exit(EXIT_FAILURE);
}
/* 開始監聽 Client 發出的連接請求。*/
if (-1 == listen(srv_socket_fd, 10))
{
printf("Listen socket ERROR.n");
ERR_MSG(errno);
exit(EXIT_FAILURE);
}
/* 初始化 Client Sock 信息存儲變量。*/
struct sockaddr cli_sock_addr;
memset(&cli_sock_addr, 0, sizeof(cli_sock_addr));
int cli_sockaddr_len = sizeof(cli_sock_addr);
int cli_socket_fd = 0;
int recv_len = 0;
char buff[BUF_LEN] = {0};
/* 永遠接受 Client 的連接請求。*/
while (1)
{
if (-1 == (cli_socket_fd = accept(srv_socket_fd,
(struct sockaddr *)(&cli_sock_addr), // 填充 Client Sock 信息。
(socklen_t *)&cli_sockaddr_len)))
{
printf("Accept connection from client ERROR.n");
ERR_MSG(errno);
exit(EXIT_FAILURE);
}
/* 接收指定 Client Socket 發出的數據,*/
if ((recv_len = recv(cli_socket_fd, buff, BUF_LEN, 0)) < 0)
{
printf("Receive from client ERROR.n");
ERR_MSG(errno);
exit(EXIT_FAILURE);
}
printf("Recevice data from client: %sn", buff);
/* 將收到的數據重新發送給指定的 Client Socket。*/
send(cli_socket_fd, buff, recv_len, 0);
printf("Send data to client: %sn", buff);
/* 每處理完一次 Client 請求,即關閉連接。*/
close(cli_socket_fd);
memset(buff, 0, BUF_LEN);
}
close(srv_socket_fd);
return EXIT_SUCCESS;
}
客戶端
#include
#include
#include
#include
#include
#include
#include
#define ERR_MSG(err_code) do {
err_code = errno;
fprintf(stderr, "ERROR code: %d n", err_code);
perror("PERROR message");
} while (0)
const int BUF_LEN = 100;
int main(void)
{
/* 配置 Server Sock 信息。*/
struct sockaddr_in srv_sock_addr;
memset(&srv_sock_addr, 0, sizeof(srv_sock_addr));
srv_sock_addr.sin_family = AF_INET;
srv_sock_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
srv_sock_addr.sin_port = htons(6666);
int cli_socket_fd = 0;
char send_buff[BUF_LEN];
char recv_buff[BUF_LEN];
/* 永循環從終端接收輸入,并發送到 Server。*/
while (1) {
/* 創建 Client Socket。*/
if (-1 == (cli_socket_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)))
{
printf("Create socket ERROR.n");
ERR_MSG(errno);
exit(EXIT_FAILURE);
}
/* 連接到 Server Sock 信息指定的 Server。*/
if (-1 == connect(cli_socket_fd,
(struct sockaddr *)&srv_sock_addr,
sizeof(srv_sock_addr)))
{
printf("Connect to server ERROR.n");
ERR_MSG(errno);
exit(EXIT_FAILURE);
}
/* 從 stdin 接收輸入,再發送到建立連接的 Server Socket。*/
fputs("Send to server> ", stdout);
fgets(send_buff, BUF_LEN, stdin);
send(cli_socket_fd, send_buff, BUF_LEN, 0);
memset(send_buff, 0, BUF_LEN);
/* 從建立連接的 Server 接收數據。*/
recv(cli_socket_fd, recv_buff, BUF_LEN, 0);
printf("Recevice from server: %sn", recv_buff);
memset(recv_buff, 0, BUF_LEN);
/* 每次 Client 請求和響應完成后,關閉連接。*/
close(cli_socket_fd);
}
return EXIT_SUCCESS;
}
測試
編譯:
$ gcc -g -std=c99 -Wall tcp_server.c -o tcp_server
$ gcc -g -std=c99 -Wall tcp_client.c -o tcp_client
運行:
- 先啟動 TCP Server:
$ ./tcp_server
- 查看監聽 Socket 是否綁定成功:
$ netstat -lpntu | grep 6666
tcp 0 0 0.0.0.0:6666 0.0.0.0:* LISTEN 28675/./tcp_server
- 啟動 TCP Client
$ ./tcp_client
UDP Socket 編程示例
·
服務端
#include
#include
#include
#include
#include
#include
#include
#define BUF_LEN 100
int main(void)
{
int ServerFd;
char Buf[BUF_LEN] = {0};
struct sockaddr ClientAddr;
struct sockaddr_in ServerSockAddr;
int addr_size = 0;
int optval = 1;
/* 創建 UDP 服務端 Socket */
if ( -1 == (ServerFd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)))
{
printf("socket error!n");
exit(1);
}
/* 設置服務端信息 */
memset(&ServerSockAddr, 0, sizeof(ServerSockAddr)); // 給結構體ServerSockAddr清零
ServerSockAddr.sin_family = AF_INET; // 使用IPv4地址
ServerSockAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 自動獲取IP地址
ServerSockAddr.sin_port = htons(1314); // 端口
// 設置地址和端口號可以重復使用
if (setsockopt(ServerFd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
{
printf("setsockopt error!n");
exit(1);
}
/* 綁定操作,綁定前加上上面的socket屬性可重復使用地址 /
if (-1 == bind(ServerFd, (struct sockaddr)&ServerSockAddr, sizeof(ServerSockAddr)))
{
printf("bind error!n");
exit(1);
}
addr_size = sizeof(ClientAddr);
while (1)
{
/* 接受客戶端的返回數據 */
int str_len = recvfrom(ServerFd, Buf, BUF_LEN, 0, &ClientAddr, &addr_size);
printf("客戶端發送過來的數據為:%sn", Buf);
/* 發送數據到客戶端 */
sendto(ServerFd, Buf, str_len, 0, &ClientAddr, addr_size);
/* 清空緩沖區 */
memset(Buf, 0, BUF_LEN);
}
close(ServerFd);
return 0;
}
客戶端
#include
#include
#include
#include
#include
#include
#define BUF_LEN 100
int main(void)
{
int ClientFd;
char Buf[BUF_LEN] = {0};
struct sockaddr ServerAddr;
int addr_size = 0;
struct sockaddr_in ServerSockAddr;
/* 創建客戶端socket */
if (-1 == (ClientFd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)))
{
printf("socket error!n");
exit(1);
}
/* 向服務器發起請求 */
memset(&ServerSockAddr, 0, sizeof(ServerSockAddr));
ServerSockAddr.sin_family = PF_INET;
ServerSockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
ServerSockAddr.sin_port = htons(1314);
addr_size = sizeof(ServerAddr);
while (1)
{
printf("請輸入一個字符串,發送給服務端:");
gets(Buf);
/* 發送數據到服務端 /
sendto(ClientFd, Buf, strlen(Buf), 0, (struct sockaddr)&ServerSockAddr, sizeof(ServerSockAddr));
/* 接受服務端的返回數據 */
recvfrom(ClientFd, Buf, BUF_LEN, 0, &ServerAddr, &addr_size);
printf("服務端發送過來的數據為:%sn", Buf);
memset(Buf, 0, BUF_LEN); // 重置緩沖區
}
close(ClientFd); // 關閉套接字
return 0;
}
測試
運行:
$ netstat -lpntu | grep 1314
udp 0 0 0.0.0.0:1314 0.0.0.0:* 29729/./udp_server
審核編輯 黃宇
-
服務器
+關注
關注
12文章
9306瀏覽量
86069 -
Socket
+關注
關注
0文章
212瀏覽量
34889 -
網絡編程
+關注
關注
0文章
72瀏覽量
10101 -
BSD
+關注
關注
0文章
31瀏覽量
10443
發布評論請先 登錄
相關推薦
評論