包括套接字相關函數socket、bind、listen、accept、recv、send、connect;以及IO多路復用函數select和epoll的簡介 ...
1.socket 通信
1.1 大小端轉換
- 主機位元組序 16 位值 <==> 網路位元組序 16 位值
- 主機位元組序 32 位值 <==> 網路位元組序 32 位值
#include <arpa/inet.h> // 主機位元組序轉換為網路位元組序 uint16_t htons(uint16_t hostshort); // host to net unsigned short 可用埠轉換 unit32_t htonl(unit32_t hostlong); // host to net unsigned int 可用ip地址轉換 // 網路位元組序轉換為主機位元組序 uint16_t ntohs(uint16_t netshort); unit32_t ntohl(unit32_t netlong);
1.2 IP地址轉換
- 主機位元組序的字元串IP地址 <==> 網路位元組序的整形IP地址
#include <arpa/inet.h> // 主機位元組序IP to 網路位元組序(大端)IP int inet_pton(int af, const char* src, void* dst); /* 參數: af: 地址族協議 AF_INET(ipv4), AF_INET6(ipv6) src: 主機位元組序的字元串類型的IP地址,被轉換的數據 dst: 傳出參數, 存儲轉換之後的大端的IP地址 返回值: 成功0; 失敗-1 */ const char *int_ntop(int af, const void *src, char *dst, socklen_t size); /* 參數: af: 地址族協議 AF_INET; AF_INET6 src: 傳入參數, 要被轉換的數據指針, 指向記憶體中存儲的大端IP地址(整形數) dst: 傳出參數, 指針指向主機位元組序, 字元串類型的IP地址 size: dst指向的記憶體的大小 返回值: 成功: 返回指向 dst 指針指向的記憶體 失敗: NULL */
1.3 套接字相關函數
1.3.1 socket 創建
#include <arpa/inet.h> // 該頭文件包括了 <sys/socket.h> int socket(int domain, int type, int protocol); /* 參數: domain: AF_INET; AF_INET6 type: SOCK_STREAM: 流式傳輸協議 TCP SOCK_DGRAM: 報式傳輸協議 UDP protocol: 預設寫0 流式傳輸預設 TCP 報式傳輸預設 UDP 返回值: 成功: 返迴文件描述符 失敗: 返回-1 */
1.3.2 bind 綁定套接字
將監聽的套接字和本地IP和埠進行關聯
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); /* 參數: sockfd: 用於監聽的套接字, 通過socket創建 addr: 將本地ip和埠初始化給該結構體(需要用大端) 綁定的時候伺服器一般ip使用巨集 INADDR_ANY (0) 0 表示綁定該主機的所有ip地址, 多個網卡可能有多個ip addrlen: 記錄第二個指針指向記憶體的大小, sizeof(struct sockaddr) 返回值: 成功0, 失敗-1 */
1.3.3 listen 監聽套接字
給監聽的套接字設置監聽,開始檢測客戶端鏈接
int listen(int sockfd, int backlog); /* 參數: sockfd: 監聽的套接字, 設置監聽前需要先綁定 backlog: 可以同時檢測的新的連接個數, 最大值128 返回值: 成功0, 失敗-1 */
1.3.4 accept 接收客戶端連接
等待並接受客戶端的連接,阻塞函數,沒有客戶端連接就阻塞,監聽的文件描述符緩衝區沒有數據就阻塞,有數據就解除阻塞建立連接,連接建立成功後,返回一個通信用的文件描述符
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); /* 參數: sockfd: 監聽的文件描述符 addr: 傳出參數, 保存了建立連接的客戶端的地址信息(ip 埠) -> 大端存儲 不需要客戶端信息則填NULL addrlen: 傳入傳出參數, 傳入addr指針指向的記憶體大小, 傳出存儲了客戶端信息的addr記憶體大小 addr為NULL,則該參數也填NULL 返回值: 文件描述符或-1 */
1.3.5 read、recv 讀數據
讀取數據,如果數據區空會讀堵塞
ssize_t read(int sockfd, void *buf, size_t size); ssize_t recv(int sockfd, void *buf, size_t size, int flags); /* 參數: sockfd: 通信文件描述符 伺服器端: accept 返回值 客戶端: socket 創建得到, connect 初始化連接 buf: 存儲接收到的數據, 數據來自文件描述符對應的緩衝區 size: buf 的記憶體容量 flag: 預設屬性0即可 返回值: >0: 讀到的位元組數 =0: 對方斷開連接 -1: 讀異常, 失敗 */
1.3.6 write、send 寫數據
發送數據,如果數據區滿會寫阻塞
ssize_t write(int fd, const void *buf, size_t len); ssize_t send(int fd, const void *buf, size_t len, int flags); /* 參數: fd: 通信的文件描述符 buf: 要發送的數據緩衝區 len: 緩衝區大小 flags: 使用預設屬性0即可 */
1.3.7 recvfrom / sendto 發送接收
- 報式傳輸協議發送
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); /* 參數: sockfd: 通信文件描述符 buf: 一塊有效記憶體地址 len: 參數buf指向的記憶體地址大小 flags: 預設屬性0即可 src_addr: 傳出參數, 保存發送端的IP和埠(網路位元組序), 不感興趣可以NULL addrlen: 傳入傳出參數, src_addr指針指向記憶體空間的大小, 如果src_addr為NULL, 則填NULL 返回值: >0: 接收到的位元組數; -1: 失敗 */
- 報式傳輸協議接收
ssize_t sendto(int sockfd, void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t *addrlen); /* 參數: sockfd: 通信文件描述符 buf: 待發送的數據地址 len: 參數buf指向的記憶體地址大小 flags: 預設屬性0即可 dest_addr: 傳入參數, 接收端的IP和埠信息(網路位元組序) addrlen: 傳入參數, src_addr指針指向記憶體空間的大小 返回值: >0: 發送的位元組數; -1: 失敗 */
1.3.8 connect 客戶端連接
客戶端連接伺服器
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); /* 參數: sockfd: 通信文件描述符 addr: 連接伺服器的ip和埠信息(需要使用大端描述) addrlen: 參數addr指向的記憶體大小 返回值: 成功0; 失敗-1 */
1.4 套接字選項
該函數用來設置套接字選項,埠復用、廣播、組播等,下麵是埠復用的參數解釋
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); /* 參數 sockfd: 監聽的套接字 level: SOL_SOCKET optname: SO_REUSEPORT optval: 實際類型int 0 -> 埠不復用 1 -> 埠復用 optlen: optval 指針指向的記憶體大小 sizeof(int) 返回值 成功0, 失敗-1 */
2. IO多路復用
2.1 select
- 構造一個文件描述符列表,將要監聽的文件描述符添加到該列表中(最大支持1024,線性描述) 調用一個函數,監聽該表中的文件描述符,知道這些描述符中的一個進行IO操作時,函數返回(該函數為阻塞函數,檢測由內核完成)
- 讀集合:檢測文件描述符列表的讀緩衝區
- 監聽的文件描述符:新客戶端連接
- 通信的文件描述符:新數據到達
-
- 寫集合:內核檢測集合中文件描述符是否可寫
- 通信的文件描述符
- 異常集合:檢測文件描述符是否有異常
- 寫集合:內核檢測集合中文件描述符是否可寫
- 返回時,告訴進程有哪些描述符需要進行IO操作
#include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); /* 參數: nfds: 下麵三個集合中, 最大文件描述符值 + 1 readfds: 傳出傳出參數,讀集合,檢測若幹文件描述符的讀緩衝區(新連接 / 新數據) writefds: 傳入傳出參數,寫集合,檢測若幹文件描述符的寫緩衝區(一般都可寫,很少用) execptfds: 傳入傳出參數,異常集合 timeout: 表示時間段,最長檢測多長時間,超過這個時間還在阻塞就解除阻塞 NULL 一直阻塞等待; 0 函數調用後立刻返回 返回值: >0: 檢測完成後,滿足條件的總個數 =0: 超時強制返回 - 1: 失敗 */
timeval 結構體
struct timeval { time_t tv-sec; suseconds_t tv_usec; };
fd_set 文件描述符集合(位操作)操作函數
void FD_CLR(int fd, fd_set *set); // 刪除fd int FD_ISSET(int fd, fd_set *set); // 判斷fd是否在集合 void FD_SET(int fd, fd_set *set); // 添加fd void FD_ZERO(fd_set *set); // 清空fd(初始化)
2.2 epoll
在select/poll時代,伺服器進程每次都把這100萬個連接告訴操作系統(從用戶態複製句柄數據結構到內核態),讓操作系統內核去查詢這些套接字上是否有事件發生,輪詢完後,再將句柄數據複製到用戶態,讓伺服器應用程式輪詢處理已發生的網路事件,這一過程資源消耗較大,因此,select/poll一般只能處理幾千的併發連接。
epoll的設計和實現與select完全不同。epoll通過在Linux內核中申請一個簡易的文件系統。把原先的select/poll調用分成了3個部分:
1)調用epoll_create()建立一個epoll對象(在epoll文件系統中為這個句柄對象分配資源)
2)調用epoll_ctl向epoll對象中添加這100萬個連接的套接字
3)調用epoll_wait收集發生的事件的連接
如此一來,要實現上面說是的場景,只需要在進程啟動時建立一個epoll對象,然後在需要的時候向這個epoll對象中添加或者刪除連接。同時,epoll_wait的效率也非常高,因為調用epoll_wait時,並沒有一股腦的向操作系統複製這100萬個連接的句柄數據,內核也不需要去遍歷全部的連接。
2.2.1 epoll_create 創建 epoll
#include <sys/epoll.h> int epoll_create(int size); /* 參數: size: 沒有實際意義, 大於0即可 返回值: 成功: 返回一個文件描述符 該文件描述符對應的指針存儲了紅黑樹的根節點 失敗: -1 */
2.2.2 epoll_ctl 操作epoll
實現對 epoll 樹上節點的操作(添加、修改、刪除節點)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); /* 參數: epfd: epoll_create() 函數的返回值,找到對應的epoll實例 op: EPOLL_CTL_ADD: 添加新節點 EPOLL_CTL_MOD: 修改已經添加到樹上節點的屬性(讀改寫) EPOLL_CTL_DEL: 刪除節點 fd: 要操作的文件描述符 添加 / 修改 / 刪除(監聽、通信) event: 對應的事件(若刪除填NULL) EPOLLIN: 讀事件 EPOLLOUT: 寫事件 */
- epoll_data
typedef union epoll_data{ void *ptr; int fd; // 該聯合體常用這個 uint32_t u32; uint64_t u64; } epoll_data_t;
- epoll_event
- event 是位操作,EPOLLIN 檢測寫緩衝區,EPOLLOUT 檢測讀緩衝區
- data.fd 等於 epoll_ctl 函數調用的第三個參數
struct epoll_event{ uint32_t event; // Epoll events; epoll_data_t data; // User data variable };
2.2.3 epoll_wait
阻塞函數,委托內核檢測epoll樹上文件描述符的狀態,如果沒有狀態變化,預設一直阻塞
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); /* 參數: epfd: epoll_create() 的返回值, 找到epoll實例 event: 傳出參數,記錄了這輪檢測到epoll模型中有狀態變化的文件描述符(結構體數組地址) maxevent: events數組的容量 timeout: 超時時長 ms(-1一直阻塞; 0立即返回) 返回值: 成功: 有多少文件描述符發生變化 */
2.2.4 Level triggered 水平模式(預設)
LT(level triggered)是預設的工作方式,同時支持 block 和 no-block socket。這種模式下,內核會通知文件描述符是否就緒,如果不進行任何操作,內核會一直通知你該文件描述符就緒
2.2.5 Edge triggered 邊沿模式
ET(edge triggered)是高速工作模式,只支持 no-block socket。這種模式下,如果接到通知,但是沒有把數據從緩衝區讀完,epoll_wait不會再次通知;直到再次接收到新數據也一樣通知一次,但是此時他會接著上次的緩衝區數據讀。
struct epoll_event ev; ev.events = EPOLLIN | EPOLLET; // 設置文件描述符為邊沿模式 ev.data.fd = lfd;
使用邊沿模式讀數據需要在收到消息後我們一般需要 while(1) 死迴圈讀取數據直到緩衝區數據讀完,所以需要設置文件描述符為非阻塞狀態,讓read可以非阻塞讀取數據,通過 read 的返回值判斷是否結束該死迴圈
int fcntl(int fd, int cmd, ...); int flag = fcntl(cfd, F_GETFL); flag = flag | O_NONBLOCK; fcntl(cfd, F_SETFL, flag); //設置文件描述符為非阻塞, read函數再讀取不會阻塞
最後因為這裡已經設置為非阻塞,可以根據read的返回值判斷是否已經讀完緩衝區了,如果讀完了會有errno EAGAIN的錯誤碼,根據該錯誤碼跳出迴圈即可
while(1) { int len = recv(curfd, buf, sizeof(buf), 0); if(len > 0) printf("列印接收的數據"); else if( len == 0) printf("斷開連接"); else { if(errno==EAGAIN) { printf("數據讀完了"); break; // 跳出迴圈 } perror("接收錯誤"); exit(0); } }
3. 代碼示例
3.1 TCP、epoll伺服器
- 創建socket套接字
- 綁定ip和埠
- 設置監聽
- 初始化一個epoll樹
- 將文件描述符加入epoll樹
- 委托內核檢測文件描述符狀態
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <sys/epoll.h> int main() { // 1. 創建套接字 int lfd = socket(AF_INET, SOCK_STREAM, 0); if(lfd == -1) { perror("socket error"); exit(1); } // 2.將 套接字 和 ip埠 綁定 struct sockaddr_in addr; addr.sin_family = AF_INET; // ipv4 addr.sin_addr.s_addr= INADDR_ANY; // 0地址(本地任意地址) addr.sin_port = htons(8989); // 埠轉為大端 int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr)); if(ret == -1) { perror("bind error"); exit(2); } // 3.設置監聽 ret = listen(lfd, 128); if(ret == -1) { perror("listen error"); exit(3); } // 4.初始化檢測的集合 int epfd = epoll_create(1); if(epfd == -1) { perror("epoll_create error"); exit(4); } // 5.將要檢測的節點添加到epoll樹中 struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = lfd; ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev); if(ret == -1) { perror("epoll_ctl"); exit(5); } // 6.委托內核檢測epoll樹中的文件描述符狀態 struct epoll_event evs[1024]; int size = sizeof(evs) / sizeof(evs[0]); while(1) { int num = epoll_wait(epfd, evs, size, -1); // 把文件描述符發生變化的儲存到 evs 數組中 printf("num = %d\n", num); // 遍歷evs數組 for(int i=0; i<num; i++) { int curfd = evs[i].data.fd; if(curfd == lfd) // lfd 套接字狀態改變說明有新鏈接請求 { int cfd = accept(lfd, NULL, NULL); ev.events = EPOLLIN; ev.data.fd = cfd; epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev); // 把新的鏈接加入到epoll樹中 } else // 其他套接字狀態改變說明有新數據抵達 { char buf[1024]; memset(buf, 0, sizeof(buf)); int len = recv(curfd, buf, sizeof(buf), 0); if(len == 0) { printf("客戶端斷開了鏈接...\n"); epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL); close(curfd); } else if(len>0) { printf("recv data: %s\n"); send(curfd, buf, len, 0); } else { perror("recv error"); exit(6); } } } } }
3.2 UDP
3.2.1 伺服器
- UDP伺服器需要創建套接字
- 綁定埠
- 接收數據
- 根據接收數據的客戶端發送數據
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main() { // 1.創建通信套接字 int fd = socket(AF_INET, SOCK_DGRAM, 0); if(fd==-1) { perror("socket"); exit(0); } // 2.接收數據需要綁定固定的埠 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(8989); addr.sin_addr.s_addr = INADDR_ANY; int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr)); if(ret==-1) { perror("bind"); exit(0); } // 通信 char ip[24]; char buf[1024]; struct sockaddr_in cliaddr; int clilen = sizeof(cliaddr); while(1) { // 3.接收數據 int len = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr*)&cliaddr, &clilen); // 把發送端數據保存在cliaddr中 if(len==-1) { break; } printf("client ip: %s, port: %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, sizeof(ip)), ntohs(cliaddr.sin_port)); // 列印發送端ip和port printf("client say: %s\n", buf); // 列印發送端發送的內容 // 4.回覆數據 sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&cliaddr, clilen); } close(fd); return 0; }
3.2.2 客戶端
- UDP客戶端相對於伺服器端減少了手動綁定ip埠的步驟
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main() { // 1.創建通信套接字 int fd = socket(AF_INET, SOCK_DGRAM, 0); if(fd==-1) { perror("socket"); exit(0); } // 伺服器地址 struct sockaddr_in serveraddr; serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(8989); inet_pton(AF_INET, "10.0.2.15", &serveraddr.sin_addr.s_addr); // 通信 char ip[24]; char buf[1024]; int num=0; while(1) { // 2.發送數據 sprintf(buf, "Hello World!, %d\n", num++); sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&serveraddr, sizeof(serveraddr)); // 3.接收數據 memset(buf, 0, sizeof(buf)); int len = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL); // 把發送端數據保存在cliaddr中 if(len==-1) { break; } printf("client say: %s\n", buf); // 列印發送端發送的內容 } close(fd); return 0; }
3.3 UDP廣播
3.3.1 伺服器
- 伺服器創建socket
- 設置廣播屬性
- 向廣播ip端發送數據
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> // 伺服器就是廣播端, 不需要收數據, 自動綁定了以後發數據就行 int main() { // 1.創建socket int fd = socket(AF_INET, SOCK_DGRAM, 0); if(fd==-1) { perror("socket"); exit(0); } // 2.設置廣播屬性 int opt = 1; // 1表示允許廣播, 0表示不允許廣播 setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt)); // 初始化數據接收端地址信息 struct sockaddr_in cliaddr; cliaddr.sin_family = AF_INET; cliaddr.sin_port = htons(8989); inet_pton(AF_INET, "10.0.2.255", &cliaddr.sin_addr.s_addr); // 3.廣播發送數據 char buf[1024]; int num = 0; while(1) { sprintf(buf, "發送廣播數據: %d\n", num++); sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&cliaddr, sizeof(cliaddr)); printf("%s\n", buf); sleep(1); } close(fd); return 0; }
3.3.2 客戶端
- 客戶端創建socket
- 綁定固定的埠用來接收數據
- recvfrom接收數據
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main() { // 1.創建通信套接字 int fd = socket(AF_INET, SOCK_DGRAM, 0); if(fd==-1) { perror("socket"); exit(0); } // 綁定固定的埠 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(8989); addr.sin_addr.s_addr = INADDR_ANY; int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr)); if(ret==-1) { perror("bind"); exit(0); } // 通信 char ip[24]; char buf[1024]; while(1) { // 接收數據 memset(buf, 0, sizeof(buf)); int len = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL); // 把發送端數據保存在cliaddr中 if(len==-1) { break; } printf("boardcast say: %s\n", buf); // 列印發送端發送的內容 } close(fd); return 0; }