linux socket編程總結

在internet網絡的世界裏,socket能夠說是最重要的任務間通信的方式,尤爲是當兩個任務駐留在不一樣的機器上須要經過網絡介質鏈接。今天系統複習一下socket編程,由於本人已經有了基本的網絡和操做系統的知識,直接跳過很基本的背景知識介紹了。我理解的socket就是抽象封裝了傳輸層如下軟硬件行爲,爲上層應用程序提供進程/線程間通訊管道。就是讓應用開發人員不用管信息傳輸的過程,直接用socket API就OK了。貼個TCP的socket示意圖體會如下。php

clipboard.png

網上找了些寫的不錯的教程研究一下,着重參考The Tenouk's Linux Socket (network) programming tutorialsocket programming。重點就socket connection創建、通訊過程和高併發模式作一下深刻分析。html

Socket通訊過程和API全解析

udp和TCP socket通訊過程基本上是同樣的,只是調用api時傳入的配置不同,以TCP client/server模型爲例子看一下整個過程。linux

clipboard.png

socket API

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(),調用鏈以下。
clipboard.png網絡

詳細的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端

這是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端

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);
}

高併發socket -- select vs epoll

上面舉的server的例子是用多進程來實現併發,固然還有其餘比較高效的作法,好比IO複用。select和epoll是IO複用經常使用的系統調用,詳細分析一下。

select API

#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 API

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。

對比select和epoll

有了上面的API,咱們能夠比較直觀的比較select和epoll的特色

  1. select的memory copy比epoll多。

    • select每次調用都要有用戶空間到kernel空間的內存copy,把全部要監控配置copy到內核。

    • epoll只須要epoll_ctl配置的時候copy,並且是增量copy,epoll_wait沒有用戶空間到內核的copy

  2. select函數調用返回後的處理比epoll低效

    • select()返回給application有幾件事情發生了,可是沒說是誰有事情,application還得挨個遍歷過去,看看誰有啥事

    • epoll_wait()返回給application更多的信息,誰發生了什麼事都通知給application了,application直接處理這些事件就好了,不須要遍歷

  3. 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

相關文章
相關標籤/搜索