網絡編程基礎
- 套接字編程需要指定套接字地址作為參數,不同的協議族有不同的地址結構,比如以太網其結構為sockaddr_in。
- 通用套接字:
struct sockaddr { sa_family_t sa_family; /* address family, AF_xxx 16Bytes */ char sa_data[14]; /* 14 bytes of protocol address */ };
-
實際使用的套接字結構
- 以bind函數為例:
bind(int sockfd, //套接字文件描述符
struct sockaddr *uaddr,//套接字結構地址
int addr_len)//套接字地址結構長度
使用struct sockaddr 為通用結構體,在以太網中,一般使用結構 sockaddr_in
- 以太網套接字
/* Internet address. */ struct in_addr { __be32 s_addr; };
/* Structure describing an Internet (IP) socket address. */
#define __SOCK_SIZE__ 16 /* sizeof(struct sockaddr) */ struct sockaddr_in { __kernel_sa_family_t sin_family; /* Address family */ __be16 sin_port; /* Port number */ struct in_addr sin_addr; /* Internet address */ /* Pad to size of `struct sockaddr'. */ unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) - sizeof(unsigned short int) - sizeof(struct in_addr)]; };
- 結構 sockaddr 和結構 sockaddr_in的關
第二章:TCP網絡編程流程
tcp網絡編程主要采取C/S模式,即客戶端(C)、服務器(S)模式
- 創建網路套接字接口函數socket
int socket (int family, int type, int protocol)
int family
- AF_UNIX : Sockets for interprocess communication in the local computer.
- AF_INET : Sockets of the TCP/IP protocol family based on the Internet Protocol Version 4
- AF_INET6 : TCP/IP protocol family based on the new Internet Protocol, Version 6.
- AF_IPX : IPX protocol family.
int type
- SOCK_STREAM (stream socket) specifies a stream-oriented, reliable, in-order full duplex connection between two sockets.
- SOCK_DGRAM (datagram socket) specifies a connectionless, unreliable datagram service, where packets may be transported out of order.
- SOCK_RAW (raw socket).
int protocol
- TCP is always selected for the SOCK_STREAM socket type, and UDP is always used as the transport protocol for SOCK_DGRAM
int bind(int sockfd, struct sockaddr *uaddr, socketlen_t uaddrlen)
- sockfd為 socket()函數創建返回的fd
- uaddr 指向一個包含了ip地址 端口等信息
- uaddrlen 是sockaddr的長度
- bind 可以指定Ip地址或者端口 可以都指定
int listen(int sockfd, int backlog)
- sockf為socket創建成功返回的fd
- backlog 表示在accept 函數處理之前在等待隊列中允許最多的客戶端個數
int accept(int sockfd, struct sockaddr *addr, socketlen_t * addrlen)
- accept 函數可以得到成功連接客戶端的ip地址、端口信息和協議族等信息
- accpet返回值是新連接客戶端套接字的描述符
數據的IO和復用
常用的數據I/O函數有recv/send() readv/writev recvmsg/sendmsg
int recv(int sockfd, void *buf, size_t len, int flag)
- recv 函數的參數flag用於設置接收數據的方式
- recv函數返回成功接收到的字節數,錯誤時返回-1
int send(int sockfd, const void *buf, size_t len, int flags)
- send函數成功發送的字節數,發生錯誤是返回-1
int recvmsg (int sockfd, struct msghdr *msg, int flags)
- recvmsg 表示從sockd 中接收數據放在緩沖區,其操作方式由flags指定
- 其返回值表示成功接收到的字節數,-1時表示發生錯誤
- 當對端使用正常方式關閉連接時,返回值為0,如調用close
- flags含義:
I/O模型
I/O復用
select函數簡介
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
- maxfdp1:指定待測試的描述符個數,它的值是待測試的最大描述符加1
- readset、writeset、exceptset:指定讓內核測試讀、寫、異常條件的描述符
- timeout:最長等待時間
- timeout參數的三種可能:
a.設為空指針:永遠等待下去,僅在有描述符就緒時才返回
b.正常設置timeout,在不超過timeout設置的時間內,在有描述符就緒時返回
c.將timeout.tv_sec和timeout.tv_usec都設為0:檢查描述符后立即返回(輪詢)
非阻塞I/O
-
非阻塞connect 以及非阻塞accept
- 以及調用select 的非阻塞I/O
進程間通信
- Unix域協議
管道
#include <unistd.h>
int pipe(int pipefd[2]);
成功調用 pipe 函數之后,可以對寫入端描述符 pipefd[1] 調用 write ,向管道里面寫入數據,比如
write(pipefd[1],wbuf,count);
一旦向管道的寫入端寫入數據后,就可以對讀取端描述符 pipefd[0] 調用 read
管道有如下三條性質:
· 只有當所有的寫入端描述符都已關閉,且管道中的數據都被讀出,對讀取端描述符調用 read 函數
才會返回 0 (即讀到 EOF 標志)。
· 如果所有讀取端描述符都已關閉,此時進程再次往管道里面寫入數據,寫操作會失敗, errno 被設
置為 EPIPE ,同時內核會向寫入進程發送一個 SIGPIPE 的信號。
· 當所有的讀取端和寫入端都關閉后,管道才能被銷毀
這種管道因為沒有實體文件與之關聯,適用於有親緣關系的任意兩個進程之間通信
命名管道 FIFO
命名管道就是為了解決無名管道的這個問題而引入的。 FIFO 與管道類似,最大的差別就是有實體
文件與之關聯。由於存在實體文件,不相關的沒有親緣關系的進程也可以通過使用 FIFO 來實現進程之
間的通信;
從外表看,我是一個 FIFO 文件,有文件名,任何進程通過文件名都可以打開我
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
一旦 FIFO 文件創建好了,就可以把它用於進程間的通信了。一般的文件操作函數如 open 、 read 、 write 、 close 、 unlink 等都可以用在 FIFO 文件
上; 對 FIFO 文件推薦的使用方法是,兩個進程一個以只讀模式( O_RDONLY )打開 FIFO 文件,另一個以只寫模式( O_WRONLY )打開 FIFO 文
件。這樣負責寫入的進程寫入 FIFO 的內容就可以被負責讀取的進程讀到,從而達到通信的目的
System V 消息隊列 信號量 共享內存
管道和 FIFO 都是字節流的模型,這種模型不存在記錄邊界,如果從管道里面讀出 100
個字節,你無法確認這 100 個字節是單次寫入的 100 字節,還是分 10 次每次 10 字節寫入的,你也無法知
曉這 100 個字節是幾個消息。管道或 FIFO 里的數據如何解讀,完全取決於寫入進程和讀取進程之間的約
定;System V 消息隊列是優於管道和 FIFO 的。原因是消息隊列機
制中,雙方是通過消息來通信的,無需花費精力從字節流中解析出完整的消息;
System V 消息隊列比管道或 FIFO 優越的第二個地方在於每條消息都有 type 字段,消息的讀取進程可
以通過 type 字段來選擇自己感興趣的消息,也可以根據 type 字段來實現按消息的優先級進行讀取,而不
一定要按照消息生成的順序來依次讀取
一般來說,信號量是和某種預先定義的資源相關聯的。信號量元素的值,表示與之關聯的資源的個數
一旦將信號量和某種資源關聯起來,就起到了同步使用某種資源的功效
共享內存是所有 IPC 手段中最快的一種。它之所以快是因為共享內存一旦映射到進程的地址空間,
進程之間數據的傳遞就不須要涉及內核了。
回顧一下前面已經討論過的管道、 FIFO 和消息隊列,任意兩個進程之間想要交換信息,都必須通
過內核,內核在其中發揮了中轉站的作用:
· 發送信息的一方,通過系統調用( write 或 msgsnd )將信息從用戶層拷貝到內核層,由內核暫存這
部分信息。
· 提取信息的一方,通過系統調用( read 或 msgrcv )將信息從內核層提取到應用層
經驗:
- epoll 或者 select 處理事件時,可讀事件時,read返回值-1,如果errno不為EAGAIN,可以認為失敗,並關閉fd。read返回0,說明對方斷開連接,此時也需要關閉fd。如果鏈路斷了,如拔掉網線,需要是用keepalive來觸發可寫事件
- 本地UDP發送過快也是會丟包的。非阻塞情況下的unix domain socket哪怕是STREAM的也是會丟包的
- 使用unix socket通信相比於本地udp通信減少了校驗和的計算。使用阻塞函數時,unix domain socket可以保證不丟包不亂序,但是當發送緩沖區滿了的話則會阻塞。使用非阻塞操作時經測試會丟包
- 使用setsockopt設置發送緩沖時,SO_RCVBUF和SO_SNDBUF的最大值受系統設置限制,可以使用SO_RCVBUFFORCE和SO_SNDBUFFORCE來無視系統設置
- SIGPIPE信號,網絡編程時一定要處理該信號。同樣一般要設置的還有SO_REUSEADDR。當客戶端close連接時,若server繼續發送數據,會收到RST,繼續寫就會SIGPIPE
- 網絡編程對事件進行封裝,提供注冊回調函數,在可讀、可寫時進行函數調用。一般用法,針對非阻塞情況,初始化時將可讀事件注冊,需要寫的時候先寫,寫不下去的時候(errno=EAGAIN)再掛上可寫事件,只要發送緩沖區還有空間,就是可寫的
- 基於事件的編程框架,需要記錄最后一次成功read或write的時間,如果idletime大於閾值,直接close
-
- 服務器編程可以設置最大的fd個數,然后一次性申請FileEvent數組,之后由fd到事件查詢代價O(1)
- 針對非阻塞socket,connect返回EINPROGRESS時需要將fd加到可寫事件監視集合中,當select()或者poll()返回可寫事件時,需要用getsockopt去讀SOL_SOCKET層面的SO_ERROR選項,SO_ERROR為0表示連接成功,否則為連接失敗
- epoll ET模式的處理方式。讀:只要可讀就一直讀,一直讀到返回0,或者error = EAGAIN。寫:只要可寫就一直寫,知道數據發送完,或者errno = EAGAIN
- socket read緩沖區最大值TCP可查看”/proc/sys/net/ipv4/tcp_rmem”, udp 65536
- 實現定時器時通常辦法是select/poll/epoll接口,精度毫秒級;還有就是新增的系統調用timerfd_create 把時間變成了一個文件描述符,該“文件”在定時器超時的那一刻變得可讀,高於poll的精度
- 在主動關閉連接時,可以先shutdown(fd, SHUT_WR)關閉寫端,等對方close時再關閉讀端。這樣子的好處是如果對方已經發送了一些數據,這些數據不會漏收。這就要求對端在read返回0之后關閉連接或者shutdown寫端
- 網絡編程一種比較好的模型是“one loop per thread”,如果事件庫不是線程安全的,則需要使用pipe或者
- socketpair通知,子線程接受到通知(fd可讀)后處理,kernel 2.6.22加入了eventfd,是更好的通知方法
- TCP Nagle算法和TCP Delayed Ack機制可能會導致網絡延時(Linux 40ms, Windows 200ms),最容易產生問題的就是"Write-Write-Read”這種模型,發送端的Nagle算法和接收端的Delayed Ack會導致一直等到接收端delayed ack超時后數據才發送出去
- accept返回EMFILE,進程描述符用完了,無法創建新的socket,也無法close連接,會導致不斷通知該可讀事件,程序busy loop,cpu 100%,解決方法是事先准備一個nullfd=open(“/dev/null”),close該fd,accept,close socket,然后再nullfd=open(“/dev/null”),缺點是該方法線程不安全,多線程accept可能導致nullfd用於新socket創建,然后又處於busy loop中
- 服務器編程可以設置最大的fd個數,然后一次性申請FileEvent數組,之后由fd到事件查詢代價O(1)