linux socket编程总结

519 查看

在internet网络的世界里,socket可以说是最重要的任务间通讯的方式,尤其是当两个任务驻留在不同的机器上需要通过网络介质连接。今天系统复习一下socket编程,因为本人已经有了基本的网络和操作系统的知识,直接跳过很基本的背景知识介绍了。我理解的socket就是抽象封装了传输层以下软硬件行为,为上层应用程序提供进程/线程间通信管道。就是让应用开发人员不用管信息传输的过程,直接用socket API就OK了。贴个TCP的socket示意图体会以下。

网上找了些写的不错的教程研究一下,着重参考The Tenouk's Linux Socket (network) programming tutorialsocket programming。重点就socket connection建立、通信过程和高并发模式做一下深入分析。

Socket通信过程和API全解析

udp和TCP socket通信过程基本上是一样的,只是调用api时传入的配置不一样,以TCP client/server模型为例子看一下整个过程。

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()

       #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()

   #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()

   #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()

   #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