在internet網絡的世界裏,socket能夠說是最重要的任務間通信的方式,尤爲是當兩個任務駐留在不一樣的機器上須要經過網絡介質鏈接。今天系統複習一下socket編程,由於本人已經有了基本的網絡和操做系統的知識,直接跳過很基本的背景知識介紹了。我理解的socket就是抽象封裝了傳輸層如下軟硬件行爲,爲上層應用程序提供進程/線程間通訊管道。就是讓應用開發人員不用管信息傳輸的過程,直接用socket API就OK了。貼個TCP的socket示意圖體會如下。php
網上找了些寫的不錯的教程研究一下,着重參考The Tenouk's Linux Socket (network) programming tutorial和socket programming。重點就socket connection創建、通訊過程和高併發模式作一下深刻分析。html
udp和TCP socket通訊過程基本上是同樣的,只是調用api時傳入的配置不同,以TCP client/server模型爲例子看一下整個過程。linux
socket: establish socket interface
gethostname: obtain hostname of system
gethostbyname: returns a structure of type hostent for the given host name
bind: bind a name to a socket
listen: listen for connections on a socket
accept: accept a connection on a socket
connect: initiate a connection on a socket
setsockopt: set a particular socket option for the specified socket.
close: close a file descriptor
shutdown: shut down part of a full-duplex connection編程
1. socket()api
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol); - 參數說明 domain: 設定socket雙方通訊協議域,是本地/internet ip4 or ip6 Name Purpose Man page AF_UNIX, AF_LOCAL Local communication unix(7) AF_INET IPv4 Internet protocols ip(7) AF_INET6 IPv6 Internet protocols ipv6(7) type: 設定socket的類型,經常使用的有 SOCK_STREAM - 通常對應TCP、sctp SOCK_DGRAM - 通常對應UDP SOCK_RAW - protocol: 設定通訊使用的傳輸層協議 經常使用的協議有IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,能夠設置爲0,系統本身選定。注意protocol和type不是隨意組合的。
socket() API是在glibc中實現的,該函數又調用到了kernel的sys_socket(),調用鏈以下。網絡
詳細的kernel實現我沒有去讀,大致上這樣理解。調用socket()會在內核空間中分配內存而後保存相關的配置。同時會把這塊kernel的內存與文件系統關聯,之後即可以經過filehandle來訪問修改這塊配置或者read/write socket。操做socket就像操做file同樣,應了那句unix一切皆file。提示系統的最大filehandle數是有限制的,/proc/sys/fs/file-max設置了最大可用filehandle數。固然這是個linux的配置,能夠更改,方法參見Increasing the number of open file descriptors,有人作到過1.6 million connection。併發
2. bind()app
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 參數說明 sockfd:以前socket()得到的file handle addr:綁定地址,可能爲本機IP地址或本地文件路徑 addrlen:地址長度 功能說明 bind()設置socket通訊的地址,若是爲INADDR_ANY則表示server會監聽本機上全部的interface,若是爲127.0.0.1則表示監聽本地的process通訊(外面的process也接不進啊)。
3. listen()dom
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog); 參數說明 sockfd:以前socket()得到的file handle backlog:設置server能夠同時接收的最大連接數,server端會有個處理connection的queue,listen設置這個queue的長度。 功能說明 listen()只用於server端,設置接收queue的長度。若是queue滿了,server端能夠丟棄新到的connection或者回復客戶端ECONNREFUSED。
4. accept()socket
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 參數說明: addr:對端地址 addrlen:地址長度 功能說明: accept()從queue中拿出第一個pending的connection,新建一個socket並返回。 新建的socket咱們叫connected socket,區別於前面的listening socket。 connected socket用來server跟client的後續數據交互,listening socket繼續waiting for new connection。 當queue裏沒有connection時,若是socket經過fcntl()設置爲 O_NONBLOCK,accept()不會block,不然通常會block。
疑問:kernel是如何區分listening socket和connected socket的呢??雖然兩者的五元組是不同的,kernel如何知道經過哪一個socket跟APP交互?經過解析內容,是SYN仍是數據?暫時存疑。
5. connect()
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 參數說明: sockfd: socket的標示filehandle addr:server端地址 addrlen:地址長度 功能說明: connect()用於雙方鏈接的創建。 對於TCP鏈接,connect()實際發起了TCP三次握手,connect成功返回後TCP鏈接就創建了。 對於UDP,因爲UDP是無鏈接的,connect()能夠用來指定要通訊的對端地址,後續發數據send()就不須要填地址了。 固然UDP也能夠不使用connect(),socket()創建後,在sendto()中指定對端地址。
這是TCP server代碼例子,server收到client的任何數據後再回返給client。主進程負責accept()新進的connection並建立子進程,子進程負責跟client通訊。
#include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <unistd.h> #define MAXLINE 4096 /*max text line length*/ #define SERV_PORT 3000 /*port*/ #define LISTENQ 8 /*maximum number of client connections */ int main (int argc, char **argv) { int listenfd, connfd, n; socklen_t clilen; char buf[MAXLINE]; struct sockaddr_in cliaddr, servaddr; //creation of the socket listenfd = socket (AF_INET, SOCK_STREAM, 0); //preparation of the socket address servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); // bind address bind (listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)); // connection queue size 8 listen (listenfd, LISTENQ); printf("%s\n","Server running...waiting for connections."); while(1) { clilen = sizeof(cliaddr); connfd = accept (listenfd, (struct sockaddr *) &cliaddr, &clilen); printf("%s\n","Received request..."); if (!fork()) { // this is the child process close(listenfd); // child doesn't need the listener while ( (n = recv(connfd, buf, MAXLINE,0)) > 0) { printf("%s","String received from and resent to the client:"); puts(buf); send(connfd, buf, n, 0); if (n < 0) { perror("Read error"); exit(1); } } close(connfd); exit(0); } } //close listening socket close (listenfd);
}
TCP端代碼,單進程。client與server創建連接後,從標準輸入獲得數據發給server並等待server的回傳數據並打印輸出,而後等待標準輸入...
#include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <arpa/inet.h> #define MAXLINE 4096 /*max text line length*/ #define SERV_PORT 3000 /*port*/ int main(int argc, char **argv) { int sockfd; struct sockaddr_in servaddr; char sendline[MAXLINE], recvline[MAXLINE]; //basic check of the arguments if (argc !=2) { perror("Usage: TCPClient <IP address of the server"); exit(1); } //Create a socket for the client //If sockfd<0 there was an error in the creation of the socket if ((sockfd = socket (AF_INET, SOCK_STREAM, 0)) <0) { perror("Problem in creating the socket"); exit(2); } //Creation of the socket memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr= inet_addr(argv[1]); servaddr.sin_port = htons(SERV_PORT); //convert to big-endian order //Connection of the client to the socket if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr))<0) { perror("Problem in connecting to the server"); exit(3); } while (fgets(sendline, MAXLINE, stdin) != NULL) { send(sockfd, sendline, strlen(sendline), 0); if (recv(sockfd, recvline, MAXLINE,0) == 0){ //error: server terminated prematurely perror("The server terminated prematurely"); exit(4); } printf("%s", "String received from the server: "); fputs(recvline, stdout); } exit(0); }
上面舉的server的例子是用多進程來實現併發,固然還有其餘比較高效的作法,好比IO複用。select和epoll是IO複用經常使用的系統調用,詳細分析一下。
#include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout); //fd_set類型示意 typedef struct { unsigned long fds_bits[1024 / 64]; // 8bytes*16=128bytes } fd_set; 參數說明: readfds: 要監控可讀的sockets集合,看是否可讀 writefds:要監控可寫的sockets集合,看是否可寫 exceptfds:要監控發生exception的sockets集合,看是否有exception nfds:上面三個sockets集合中最大的filehandle+1 timeout:阻塞的時間,0表示不阻塞,null表示無限阻塞 功能說明: 調用select()實踐上是往kernel註冊3組sockets監控集合,任何一個或多個sockets ready(狀態跳變,不可讀變可讀 or 不可寫變可寫 or exception發生), 函數就會返回,不然一直block直到超時。 返回值>0表示ready的sockets個數,0表示超時,-1表示error。
epoll由3個函數協調完成,把整個過程分紅了建立,配置,監控三步。
step1 建立epoll實體
#include <sys/epoll.h> int epoll_create(int size); 參數說明: size:隨便給個>0的數值,如今系統不care了。 功能說明: epoll_create()在kernel內部分配了一塊內存並關聯到文件系統,函數調用成功會返回一個file handle來標識這塊內存。 #include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
Step2 配置監控的socket集合
#include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; 參數說明: epfd:前面epoll_create()建立實體的標識 op:操做符,EPOLL_CTL_ADD/EPOLL_CTL_MOD/EPOLL_CTL_DEL fd:要監控的socket對應的file handle event:要監控的事件鏈表 功能說明: epoll_ctl()配置要對哪一個socket作什麼樣的事件監控。
step3 監控sockets
#include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 參數說明: epfd:epoll實體filehandle標識 events:指示發生的事情。application分配一塊內存用event指針來指向,epoll_wait()調用時kernel將發生的事件存入event這塊內存。 maxevents:最大可接收多少event timeout:超時時間,0表示當即返回,函數不block,-1表示無限block。 功能說明: epoll_wait()真正開始監控以前設置好的sockets集合。若是有事件發生,經過事件鏈表的方式返回給application。
有了上面的API,咱們能夠比較直觀的比較select和epoll的特色
select的memory copy比epoll多。
select每次調用都要有用戶空間到kernel空間的內存copy,把全部要監控配置copy到內核。
epoll只須要epoll_ctl配置的時候copy,並且是增量copy,epoll_wait沒有用戶空間到內核的copy
select函數調用返回後的處理比epoll低效
select()返回給application有幾件事情發生了,可是沒說是誰有事情,application還得挨個遍歷過去,看看誰有啥事
epoll_wait()返回給application更多的信息,誰發生了什麼事都通知給application了,application直接處理這些事件就好了,不須要遍歷
select相比epoll有處理socket數量的限制
select內核限定了1024最大的filehandle數,若是要修改須要編譯內核
epoll沒有固定的限制,能夠達到系統最大filehandle數
小結一下二者的對比,一般能夠看到epoll的效率更高,尤爲是在大量socket併發的時候。有人說在少許sockets,好比10多個之內,select要有優點,我沒有驗證過。不過這麼少的併發用哪一個都行,不會差異太大。
The Tenouk's Linux Socket (network) programming tutorial
Beej's Guide to Network Programming
socket programming
linux內核中socket的建立過程源碼分析
how-to-use-epoll-a-complete-example-in-c
epoll manual
select manual