socket程式設計常用函式及引數
socket()
為通訊建立一個端點,為套接字返回一個檔案描述符
int socket(int domain, int type, int protocol);
domain 為套接字指定協議集
- AF_UNIX, AF_LOCAL
AF_UNIX=AF_LOCAL本地socket,用於本機程序間通訊 - AF_INET
表示IPv4協議 - AF_INET6
表示IPv6協議 - AF_NETLINK
用於kernel和user之間的通訊,比如使用者空間接受核心發出的訊息事件
type
- SOCK_STREAM
可靠的面向流服務或流套接字,TCP - SOCK_DGRAM
資料報文服務或者資料報文套接字 UDP - SOCK_RAW
在網路層之上自行指定運輸層協議頭,即原始套接字
2.6.67核心之後,type引數又添加了一個新的目的: 用於定義socket的行為 - SOCK_NONBLOCK
建立的socket預設是阻塞的,該引數改變修改socket的屬性為非阻塞 - SOCK_CLOEXEC
建立子程序時,檔案描述符自動被關閉,不會被clone給子程序。這樣做的原因如下:有時我們建立子程序後會使用exec來代替當前的子程序,但是在這之前我們需要把子程序中無用的檔案描述符都關閉掉,可以手動關閉,但是如果在複雜的大型專案中,這樣做顯然不現實,使用這個引數,子程序不會把父程序的檔案描述符clone過來,也就省去了關閉的煩惱
protocol 實際使用的傳輸協議
- 0
根據domain和type選擇預設協議 - IPPROTO_TCP
傳輸控制協議 - IPPROTO_SCTP
流控制傳輸協議 - IPPROTO_UDP
使用者資料包協議 - IPPROTO_DCCP
資料擁塞控制協議
bind()
為套接字分配地址,當使用socket建立套接字後,是賦予了其使用的協議,並沒有分配地址。在接受其它的主機連線之前,必須先呼叫bind為套接字分配一個地址
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
sockfd 使用bind函式的套接字描述符
addr 指向sockaddr結構的指標
addrlen sockaddr結構的長度
- struct sockaddr 用於相容各種協議族的地址
struct sockaddr {
unsigned short sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
- struct sockaddr_in 網路協議
struct sockaddr_in {
short int sin_family; /* Address family */
unsigned short int sin_port; /* 埠號 一些協議都有指定的埠號 */
struct in_addr sin_addr; /* 網路地址 htonl(INADDR_ANY)表示所有的網絡卡,一般用於服務端 */
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
};
struct in_addr {
unsigned long s_addr;/* 4個位元組,用於存放ip地址,如 192 168 9 6,每個數字佔用一個位元組 */
};
- struct sockaddr_nl NETLINK協議
struct sockaddr_nl {
sa_family_t nl_family; /* Address family AF_NETLINK */
unsigned short nl_pad; /* zero */
pid_t nl_pid; /* getpid() */
u_32 nl_groups;
}nl;
listen()
int listen(int sockfd, int backlog);
socket繫結地址之後,預設是想要去連線的,liste會將socket的主動連線的預設屬性改為被動連線屬性。然後開始監聽可能的連線請求,但是這隻能在有可靠資料流保證的時候(TCP)使用,如SOCK_STREAM和SOCK_SEQPACKET
sockfd 使用socke建立的描述符
backlog 監聽佇列的大小,每當有一個連線請求到來,就會進入此監聽佇列。當一個請求被accept()接受,這個請求就會被移除對列。當佇列滿後,新的連線請求就會返回錯誤,一般發生在同時有大量請求到來,伺服器處理不過來的時候。
connect()
用於TCP連線,為一個套接字設定連線,引數有檔案描述符和想要連線的伺服器地址。
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
sockfd 客戶端的socket
addr 伺服器端的地址資訊
addrlen 伺服器端地址資訊的大小
select() && poll() && epoll()
緩衝I/O又被稱為標準I/O,大多數系統的預設I/O操作都是緩衝I/O。在linux的緩衝I/O機制中,作業系統會將I/O的資料快取在檔案系統的頁緩衝中,也就是說I/O中的資料會先拷貝到核心的緩衝區中,然後再從核心緩衝區拷貝到使用者空間。比如從網路上接收的資料會先被快取到核心的頁緩衝區,然後再將資料從核心空間複製到使用者空間。
每一次I/O操作都是核心空間和使用者空間資料拷貝的操作,這種資料拷貝操作所帶來的的CPU以及記憶體開銷是非常大的。
上面3個函式都是用來實現I/O多路複用的,I/O多路複用指的是在一個執行緒中實現對多個套接字的處理
select()
同時監視多個描述符的read/write狀態,
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
nfds
所有檔案描述符的範圍,即所有檔案描述符的最大值+1。可以通過遍歷所有listen的fd比較大小來找到最大值
readfds
需要監視那些檔案描述符是否可讀
writefds
需要監視那些檔案描述符是否可寫
exceptfds
需要監視哪些檔案描述符時候可執行
timeout
等待超時的時間
返回值
- 當監視的檔案描述符的狀態的滿足時,返回滿足條件的檔案件描述符的個數
- 當沒有滿足條件的檔案描述符,並且timeout監控時間超時,返回 0,可以理解為滿足條件的檔案描述符的個數為0
- 出錯返回-1,同時設定errno
操作檔案描述符集合fs_set的巨集:
FD_SET(fd, fdsetp) /* 將fd新增到集合fdsetp */
FD_CLR(fd, fdsetp) /* 刪除集合fdsetp中的fd */
FD_ISSET(fd, fdsetp) /* 檢視集合fdsetp中是否存在fd */
FD_ZERO(fdsetp) /* 將fdsetp清零 */
poll()
poll與select相似,都是對多個描述符的狀態進行輪詢,根據描述符的狀態進行處理。但是poll沒有沒有最大檔案描述符數量的限制。poll和select的一個缺點就是:每次輪詢都要講大量檔案描述的陣列在核心空間和使用者空間進行整體複製,而不管這些檔案描述符是否就緒,它的開銷隨著檔案描述數量的增加而線性增大
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds
pollfd結構體指定了一個被監視的檔案描述符,可以通過陣列struct pollfd fsd[MAX]傳遞多個結構體,這樣可以使poll()監視多個檔案描述符
struct pollfd {
int fd; /* 檔案描述符 */
/* POLLIN 有普通資料可讀
* POLLRDNORM 有優先資料可讀
* POLLPRI 有緊急資料可讀
* POLLOUT 寫普通資料不會被阻塞
* POLLWRNORM 寫優先資料不會被阻塞
* POLLWRBAND 寫緊急資料不會被阻塞
* POLLMSGSIGPOLL 訊息可用
* revents域中還可能返回下列事件
* POLLER 指定的檔案描述符發生錯誤
* POLLHUP 指定的描述符被掛起
* POLLVAL 指定的檔案描述符非法
*/
short events; /* 等待的事件 */
short revents; /* 實際發生的事件 */
}
nfds struct pollfd陣列的大小
timeout 超時時間 微妙
返回值
- 當監視的檔案描述符的狀態的滿足時,返回滿足條件的檔案件描述符的個數
epoll
/* 建立一個epoll控制代碼,size用來告訴核心這個監聽的資料是多大,這個引數不同於select中的
* 第一個引數,給出最大監聽的fd+1,引數size並不是限制了epoll所能監聽的描述符的最大個
* 數,只是對核心初始分配內部資料結構的一個建議 */
int epoll_create(int size);
建立好epoll後,它就會佔用一個fd值,在linux下如果檢視/proc/程序id/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須呼叫close關閉,否則可能導致fd被耗盡
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd
epoll_create()的返回值
op
操作,用三個巨集來表示
- EPOLL_CTL_ADD 新增對fd事件的監聽
- EPOLL_CTL_DEL 刪除對fd事件的監聽
- EPOLL_CTL_MOD 修改對fd事件的監聽
fd
要進行操作的套接字描述符
event
告訴核心需要監聽什麼事件
struct epoll_event {
/* EPOLLIN 檔案描述符可讀
* EPOLLOUT 檔案描述符可寫
* EPOLLPRI 檔案描述符有緊急資料可讀
* EPOLLERR 檔案描述符發生錯誤
* EPOLLHUP 檔案描述符被結束通話
* EPOLLET 將epoll設定為邊緣觸發(Edge Triggered)模式,相對於水平觸發(Level Triggered)來說的
* EPOLLONESHOT 只對檔案描述符進行一次監聽,當監聽完這次事件後,如果還要繼續監聽這個socket的話,需要再次新增到監聽佇列
*/
__unit32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
}
/* 等待epfd上的事件,最多返回maxevents個事件 */
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
epfd
epoll_create()的返回值
events
用於從核心接收得到的事件的集合
maxevents
告訴核心這個events有多大,這個maxevents的值不能大於建立epoll_create()時的size
timeout
超時時間,毫秒
返回值
- 成功返回滿足條件的檔案描述符的個數
- 超時返回0
- 錯誤返回-1,並設定errno的值
epoll的兩種工作模式
Level Trigger(LT)
這是epoll的預設工作方式,同時支援block和no-block socket。在這種做法中,核心通過epoll()告訴你一個檔案描述符已經就緒,然後如果你不對就緒的fd進行I/O操作,核心是會繼續通知你的
Edge Trigger(ET)
這是高速工作模式,只支援no-block socket。在這種做法中,核心通過epoll()告訴你一個檔案描述符已經就緒,然後它會假設你已經知道檔案描述符就緒了,並且不會再為那個檔案描述符做就緒通知,知道你做了某些操作導致那個檔案描述符不在處於就緒狀態。注意,如果一直不對這個fd進行I/O操作(從而導致它再次變成未就緒狀態),核心不會發送更多的通知,也就是核心每次只為fd傳送一次就緒通知,這樣更高效
ET模式在很大程度減少了epoll事件被重複觸發的次數,因此效率要比LT模式要高。epoll工作在ET模式時必須使用非阻塞套介面,以避免由於一個檔案描述符的阻塞讀/寫操作把處理多個檔案描述符的任務餓死
select poll 機制和epoll機制比較:
相同
使用select和poll機制在進行等待時,所線上程都是被移除CPU排程佇列的,不會耗費CPU資源的。當等待的事件發生時,執行緒被喚醒,然後進行事件的處理工作
不同
- select和poll的不同主要在與poll沒有最大檔案描述符的限制,其它基本都相同
- 當執行緒被喚醒時,poll()和select()需要輪詢所有的描述符,來找到喚醒事件,複雜度為O(n)會隨著監聽的套接字的個數的增加而線性增加,而epoll會把那個套接字發生了怎樣的I/O事件通知我們,複雜度為O(1)
accept()
當應用程式接收到來自其它主機的面對資料流的連線時,通過事件通知它(比如unix的 select()系統呼叫)。必須用accept()初始化連線。accept為每個連線建立新的套接字,並從監聽佇列中移除這個連線。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockefd 監聽的套接字描述符
addr 客戶端地址結構體資訊,一個指向sockaddr的指標
addrlen客戶端地址結構體的大小