ZMonster's Blog 巧者劳而智者忧,无能者无所求,饱食而遨游,泛若不系之舟

《TCP/IP Socket in C》阅读笔记

说明

Linux下的Socket编程,《TCP/IP Socket in C》一书阅读笔记。

套接字的创建和关闭

创建套接字

建立网络套接字需要调用函数socket(),该函数的原型为:

1: int socket(int protocol_family, int type, int protocol)

使用这个函数需要包含两个头文件:

1: #include <sys/types.h>
2: #include <sys/socket.h>

后续的相关API使用,也要在包含这两个头文件的前提下进行。

第一个参数决定套接字的协议族,一般就使用 PF_INET ,这个协议族是IPv4的网络协议族,如果要使用IPv6的网络协议,就使用 PF_INET6 。在很多地方会说明这个参数一般使用 AF_INET ,实际上"AF"是"Address Family"的缩写而"PF"是"Protocol Family"的缩写,严格来说应该在这个函数中使用"PF_XXX",而在设定地址的时候才用"AF_XXX",但实际上使用"AF_XXX"也是可以的,在socket.h中有如下定义:

1: #define PF_INET AF_INET

执行man socket也可以发现其中内容使用的都是"AF_XXX"。

第二个参数确定套接字的类型,常用的两个值是 SOCK_STREAMSOCK_DGRAM ,分别对应数流格式(TCP)和数据报格式(UDP)。

第三个参数用于自定端到端的特定协议,常用的协议有: IPPROTO_TCP, IPPROTO_UDP, IPPROTO_SCTP, IPPROTO_TIPC 等。协议和套接字类型是不能随意组合的,如 SOCK_STREAM 就不能和 IPPROTO_UDP 一起使用。当该参数置零时,会自动选择与套接字类型对应的 默认协议

如果套接字创建成功,该函数返回一个套接字描述符,否则返回-1,并将错误变量 errno 设置为对应的值(详情可man socket)。

关闭套接字

关闭套接字需要调用函数close(),该函数的原型为:

1: int close(int fd)

实际上这个函数并不是专用于关闭套接字的,而是用于关闭"文件",对于Linux这样将一切设备视为文件的系统来说,这是一个通用的函数。

该函数只有一个参数,只要把相应的套接字描述符作为参数传入即可。套接字如果关闭成功,则返回0,否则返回-1,并会将错误变量 errno 设置为对应的值(详情可man close)。

设定地址

相关结构

  • sockaddr

    sockaddr是一个结构体,其结构为:

    1: struct sockaddr{
    2:     unsigned short sa_family;   /* Address family  */
    3:     char sa_data[14];           /* Family-specific address information*/
    4: }
    

    第一个成员设定地址族(IPv4/IPv6等),通常是设置为 AF_INET ;第二成员设定和地址族相关的地址信息,包括IP地址、端口等等信息,通常情况下并不会用到全部14个字节,这是socket API的设计者为了使socket API可以灵活应对各种不同的情况而做出的设计(具体是什么不同的情况呢?有待了解)。

  • sockaddr_in

    sockaddr_in结构是对sockaddr结构在TCP/IP应用中的具体实现,其结构如下:

    1: struct sockaddr_in {
    2:     unsigned short sin_family;  /* Internet protocol */
    3:     unsigned short sin_port;    /* Address port(16 bits) */
    4:     struct in_addr sin_addr;    /* Internet address(32 bits) */
    5:     char sin_zero[8];           /* Not used */
    6: };
    

    其中结构体in_addr为:

    1: struct in_addr{
    2:     unsigned long s_addr;       /* Internet address (32 bits) */
    3: };
    

    sockaddr_in相对sockaddr,明确定义了成员sin_port和sin_addr用于设置端口和地址,实际上只用到了14个字节中的6个字节,剩下未用的8个字节(sin_zero)通常置零。 至于s_addr为何要封装到结构体in_addr中,据stackoverflow上的回答 ,说是因为在不同的socket API中,结构in_addr中可能不只有一个成员,只是在POSIX中,该结构中只有一个成员s_addr而已。

    这篇文章 说,sockaddr是给操作系统使用的,程序员应该操作sockaddr_in而不是sockaddr,在用sockaddr_in设置好地址信息后,再将sockaddr_in数据强制转换成sockaddr传递给系统调用函数。

具体操作

以下是一般的具体操作流程示例:

1: struct sockaddr_in addr;        /* 注意,不要使用sockaddr */
2: 
3: addr.sin_family = AF_INET;
4: addr.sin_addr.s_addr = htonl(INADDR_ANY);/* 服务端,客户端调用inet_addr */
5: addr.sin_port = htons(PORT);             /* 设置端口 */
6: bzero(sin_zero, sizeof(sin_zero));       /* 将未用字节置零 */

这里的操作有几个要注意的地方:

  • 设置地址和端口时,要将数据由本机字节序转换为网络字节序,常用的函数有 htons, htonl (相应的在接收网络数据时用 ntohs, ntohl 来将数据由网络字节序转换为本机字节序)
  • 不能忘记将结构中未用的8字节数据置零,否则可能导致不可预知的错误

TCP Socket

TCP C/S模型

TCP的C/S模型如下图所示: tcp.png

TCP客户端

创建TCP客户端的基本步骤为:

  1. 调用socket()创建套接字以建立和服务端的连接

    1: int sockfd;
    2: sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    
  2. 调用connect()连接服务端

    connect()函数的原型为:

    1: int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    

    第一个参数是用socket()函数返回的套接字描述符,和创建的套接字绑定;第二个参数是一个sockaddr结构类型的指针,用来设置要连接的服务端的地址信息,实际情况中一般是用一个sockaddr_in结构类型的指针作为实参;第三个参数是地址结构的长度,即 sizeof(struct sockaddr_in)

    若绑定成功,该函数返回0,否则返回-1,并设置相应的错误号。

  3. 调用send()和recv()和服务端通信

    send()函数和recv()函数的原型很相似:

    1: ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    2: ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    

    send()和recv()默认都是阻塞的,它们的最后一个参数flags可以用来改变函数的默认行为,默认情况下置零。

    send()如果发送成功,则返回发送的数据长度(字节);否则返回-1,并将错误变量 errno 设置为相应的值。

    recv()返回接收的数据长度(字节);如果出错则返回-1并将错误变量 errno 设置对应的值;如果连接已经关闭,则返回0。

  4. 调用close()关闭连接

    在最后,不需要再和服务端通信了,就调用close()函数来关闭连接。

TCP服务端

创建TCP服务端的基本步骤为:

  1. 调用socket()创建套接字

    这一步和客户端一样,不过建立的和这个套接字的用途和客户端建立的套接字不太一样。

  2. 调用bind()将套接字绑定一个端口

    函数bind()的原型为:

    1: int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    

    第一个参数使用第1步建立的套接字,第二个参数在设置时应将地址设置为 INADDR_ANY 、将端口设置为服务端的周知端口,第三个参数则是地址结构的长度。

    绑定成功则返回0;否则返回-1,并将错误变量 errno 设置为对应的值。

  3. 调用listen()监听连接请求

    listen()函数的原型为:

    1: int listen(int sockfd, int quene_limit);
    

    调用该函数将使套接字监听TCP连接请求,当发生请求时,就将请求放入队列等待后面调用accept()来进行处理。第二个参数就是请求队列的长度上限。

    正常情况下,该函数返回0;若出错,则返回-1,并将错误变量 errno 设置为对应的值。

  4. 重复做以下事情
    • 调用accept()为每个客户端连接请求建立一个套接字

      在第1步建立的套接字并不进行实际的数据发送、接收操作,它更像一个套接字工厂,对每个新的TCP连接,accept()会创建一个新的套接字,和客户端的套接字形成一个套接字对从而建立一个通信的通道。

      accept()的原型为:

      1: int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
      

      其中第一个参数是之前说的"套接字工厂",第二个参数和第三个参数用来保存客户端的地址信息以及地址信息长度。

      正常情况下该函数返回一个可用的套接字描述符;否则返回-1并将错误变量 errno 设置为对应的值。

    • 调用send()和recv()和客户端进行通信
    • 调用close()关闭和客户端的连接

信息构建

字节序

通信的两端的本机字节序可能不一致,如果不加处理地将信息进行发送,接收方可能得到的是错误的数据。解决办法是在进行send()的时候将数据由本机字节序转换为网络字节序,而另一端在接收时将数据由网络字节序转换为本机字节序。

这些操作可以使用这些函数:

1: uint32_t htonl(uint32_t hostlong);
2: uint16_t htonl(uint16_t hostshort);
3: uint32_t ntohl(uint32_t netlong);
4: uint16_t ntohs(uint16_t netshort);

如果数据本身的最小单元是一个字节,则无须进行字节序转换处理,但如果最小单元超过一个字节,如要收发的数据中包含int类型、long类型的数据,则应当进行字节序转换处理。

字节对齐和填充

如果定义一个结构体来容纳数据,那么要注意结构体内字节对齐和填充的现象。如

1: struct Msg{
2:     int a;                      /* 4 Bytes */
3:     char b;                     /* 1 Byte */
4:     int c;                      /* 4 Bytes */
5: };

如果用该结构体容纳数据,在进行收发时,将长度计算为9字节是错误的,一个该结构类型的数据所占的实际存储空间是12字节,原因是每个类型的数据在存储时都会对齐在能整除其数据类型长度的地址处,所以成员b后会有一个3字节的空洞。

正确的结构体定义应该是这样的:

1: struct Msg{
2:     int a;
3:     int c;
4:     char b;
5: };

这样的结构体的长度就和预期的一致。

信息格式化

在实际场景中,收发的数据会有复杂的格式需求,所以需要编写专门的编码和解析函数来对发送数据进行格式化、对接收数据进行解析。

格式化的简单方法有使用结构体、使用约定的分割符(终止符)等。

UDP Socket

UDP的C/S模型

UDP的C/S模型如下图所示: udp.png

可以发现UDP的模型比TCP的模型要简单。

UDP与TCP的区别

UDP是无连接的,在进行通信前,UDP客户端程序不需要先进行connect(服务端也不需要进行listen和accept)。

TCP连接可以类比于打电话,而UDP连接可以类比于发邮件。

UDP socket只要一被建立,就可以用于和许多不同的地址之间进行通信。UDP通信不使用send()和recv()函数,而是使用sendto()和recvfrom()函数:

1: ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
2:                const struct sockaddr *dest_addr, socklen_t addrlen);
3: ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
4:                  struct sockaddr *src_addr, socklen_t *addrlen);

sendto()和recvfrom()的前四个参数和send()、recv()是一样的。sendto()的后两个参数 指定 了消息目的地的地址信息;recvfrom()的后两个参数则用于 保存 消息来源地的地址信息。

特别要注意的是recvfrom()的最后一个参数,该参数既是输入也是输出。作为输入,它指定了指针src_addr所指向的结构的大小;作为输出,它会记录消息来源的地址结构长度。如果忘记将指针addrlen对应的变量的值初始化,则会导致错误。如:

int sockfd;
struct sockaddr_in their_addr;
int their_addr_len;
char msg[256];
int msg_len = sizeof(msg);

bzero(&their_addr, sizeof(their_addr));
their_addr.sin_family = AF_INET;
their_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
their_addr.sin_port = htons(8888);

sockfd = socket(AF_INET, SOCK_DGRAM, 0);
while (1) {
    recvfrom(sockfd, msg, msg_len, 0,
             (struct sockaddr *)&their_addr, &their_addr_len);
}

是错误的——当然,这里的错误主要是指在recvfrom()前没有初始化their_addr_len这个变量,实际上上面这段代码既没有进行错误检查,功能也是不完整的。正确的应该是这样的:

 1: int sockfd;
 2: struct sockaddr_in their_addr;
 3: char msg[256];
 4: int msg_len = sizeof(msg);
 5: 
 6: bzero(&their_addr, sizeof(their_addr));
 7: their_addr.sin_family = AF_INET;
 8: their_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
 9: their_addr.sin_port = htons(8888);
10: 
11: sockfd = socket(AF_INET, SOCK_DGRAM, 0);
12: while (1) {
13:     their_addr_len = sizeof(their_addr);
14:     recvfrom(sockfd, msg, msg_len, 0,
15:              (struct sockaddr *)&their_addr, &their_addr_len);
16: }

当然,UDP和TCP最大的区别是:UDP不保证发送的信息一定能到达目的地,它只是负责发送出去,然后就不管了。

Socket选项

socket选项用于改变socket的默认行为,如改变socket的接收缓冲区的默认大小。

获取socket选项的值和设置选项的值可以使用下面两个函数:

1: int getsockopt(int sockfd, int level, int opt_name,
2:                void *opt_val, unsigned int *opt_len);
3: int setsockopt(int sockfd, int level, int opt_name,
4:                const void *opt_val, unsigned int opt_len);

信号

信号(signals)是用来通知程序“发生了某些事件(events)”的一种机制。一个信号被传递到正在运行的程序时,会被以下方式中的一种进行处理:

  1. 信号被忽略
  2. 程序被操作系统终止
  3. 一个特定的处理例程启动,用于针对信号做出具体的处理
  4. 信号被阻塞

socket编程中常用的一些信号有:

信号 触发事件 缺省行为
SIGALRM 时钟计时器到期 终止
SIGCHLD 子进程结束 忽略
SIGINT 终止符输入 终止
SIGIO 套接字准备好进行读写 忽略
SIGPIPE 对已关闭的套接字进行写操作 终止

可以使用sigaction()来改变对于某个信号的缺省处理方式:

1: int sigaction(int signum, const struct sigaction *act,
2:               struct sigaction *oldact);

第一个参数用来指示要改变缺省处理方式的信号;第二个参数则定义了对该信号的新的处理方式;第三个参数若不为空,则对该信号的原处理方式的信息会被记录于该参数中。

该函数正常时返回0,出错则返回-1。

结构体sigaction的具体结构如下:

1: struct sigaction {
2:     void (*sa_hanlder)(int);    /* Signal hanlder */
3:     sigset_t sa_mask; /* Signals to be blocked during handler execution */
4:     int sa_flags;     /* Flags to modify default behavior */
5: }

其中第一个结构成员 sa_hanlder 用来指定信号处理方法,除了用户自定义的信号处理函数,它的值还可能是两个特殊的常量 SIG_IGNSIG_DFL ,前者将使信号被忽略,后者将使信号的默认处理行为被使用。

第二个结构成员 sa_mask 用来设置额外的信号屏蔽码,设置了 sa_mask 后,除了原先默认的被屏蔽信号外,消息处理方法中还会屏蔽额外设置的信号,待处理完后,被屏蔽的信号又变回默认的。 sa_mask 本质是一个信号集,而信号集的操作则通过以下方法:

1: int sigemptyset(sigset_t *set);
2: int sigfillset(sigset_t *set);
3: int sigaddset(sigset_t *set, int signum);
4: int sigdelset(sigset_t *set, int signum);

第一个函数将信号集初始化为空集;第二个函数将集合初始化为所有信号的集合;第三个和第四个则分别用于将特定的信号加入信号集和从信号集中移除特定的信号。这四个函数的正常返回值都是0,如果出错则返回-1。

在程序中处理信号一般是这样的:

  1. 定义信号处理(struct sigaction)变量
  2. 初始化信号处理变量,实现信号处理方法
  3. 将信号处理方法和信号绑定(使用sigaction()函数)

超时检测

使用函数alarm()可以实现对连接超时的设定和处理,比如客户端连接服务器时,如果一段时间内没有连接上就认为超时。该函数原型为:

1: unsigned int alarm(unsigned int secs);

alarm()会启动一个计时器,当计时终止时,就会触发一个 SIGALRM 信号。在一个可能阻塞的函数调用前启动计时器,如果当计时终止时函数仍然阻塞,则函数会被终止并返回-1,同时错误变量 errno 被设置为 EINTR

根据这些特性,就可以在socket操作中进行超时检测了。以下是一个示例:

  1: #include <stdio.h>      /* for printf() and fprintf() */
  2: #include <sys/socket.h> /* for socket(), connect(), sendto(), and recvfrom() */
  3: #include <arpa/inet.h>  /* for sockaddr_in and inet_addr() */
  4: #include <stdlib.h>     /* for atoi() and exit() */
  5: #include <string.h>     /* for memset() */
  6: #include <unistd.h>     /* for close() and alarm() */
  7: #include <errno.h>      /* for errno and EINTR */
  8: #include <signal.h>     /* for sigaction() */
  9: 
 10: #define ECHOMAX         255     /* Longest string to echo */
 11: #define TIMEOUT_SECS    2       /* Seconds between retransmits */
 12: #define MAXTRIES        5       /* Tries before giving up */
 13: 
 14: int tries=0;   /* Count of times sent - GLOBAL for signal-handler access */
 15: 
 16: void DieWithError(char *errorMessage);   /* Error handling function */
 17: void CatchAlarm(int ignored);            /* Handler for SIGALRM */
 18: 
 19: int main(int argc, char *argv[])
 20: {
 21:     int sock;                        /* Socket descriptor */
 22:     struct sockaddr_in echoServAddr; /* Echo server address */
 23:     struct sockaddr_in fromAddr;     /* Source address of echo */
 24:     unsigned short echoServPort;     /* Echo server port */
 25:     unsigned int fromSize;           /* In-out of address size for recvfrom() */
 26:     struct sigaction myAction;       /* For setting signal handler */
 27:     char *servIP;                    /* IP address of server */
 28:     char *echoString;                /* String to send to echo server */
 29:     char echoBuffer[ECHOMAX+1];      /* Buffer for echo string */
 30:     int echoStringLen;               /* Length of string to echo */
 31:     int respStringLen;               /* Size of received datagram */
 32: 
 33:     if ((argc < 3) || (argc > 4))    /* Test for correct number of arguments */
 34:     {
 35:         fprintf(stderr,"Usage: %s <Server IP> <Echo Word> [<Echo Port>]\n", argv[0]);
 36:         exit(1);
 37:     }
 38: 
 39:     servIP = argv[1];           /* First arg:  server IP address (dotted quad) */
 40:     echoString = argv[2];       /* Second arg: string to echo */
 41: 
 42:     if ((echoStringLen = strlen(echoString)) > ECHOMAX)
 43:         DieWithError("Echo word too long");
 44: 
 45:     if (argc == 4)
 46:         echoServPort = atoi(argv[3]);  /* Use given port, if any */
 47:     else
 48:         echoServPort = 7;  /* 7 is well-known port for echo service */
 49: 
 50:     /* Create a best-effort datagram socket using UDP */
 51:     if ((sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0)
 52:         DieWithError("socket() failed");
 53: 
 54:     /* Set signal handler for alarm signal */
 55:     myAction.sa_handler = CatchAlarm;
 56:     if (sigfillset(&myAction.sa_mask) < 0) /* block everything in handler */
 57:         DieWithError("sigfillset() failed");
 58:     myAction.sa_flags = 0;
 59: 
 60:     if (sigaction(SIGALRM, &myAction, 0) < 0)
 61:         DieWithError("sigaction() failed for SIGALRM");
 62: 
 63:     /* Construct the server address structure */
 64:     memset(&echoServAddr, 0, sizeof(echoServAddr));    /* Zero out structure */
 65:     echoServAddr.sin_family = AF_INET;
 66:     echoServAddr.sin_addr.s_addr = inet_addr(servIP);  /* Server IP address */
 67:     echoServAddr.sin_port = htons(echoServPort);       /* Server port */
 68: 
 69:     /* Send the string to the server */
 70:     if (sendto(sock, echoString, echoStringLen, 0, (struct sockaddr *)
 71:                &echoServAddr, sizeof(echoServAddr)) != echoStringLen)
 72:         DieWithError("sendto() sent a different number of bytes than expected");
 73: 
 74:     /* Get a response */
 75: 
 76:     fromSize = sizeof(fromAddr);
 77:     alarm(TIMEOUT_SECS);        /* Set the timeout */
 78:     while ((respStringLen = recvfrom(sock, echoBuffer, ECHOMAX, 0,
 79:                                      (struct sockaddr *) &fromAddr, &fromSize)) < 0)
 80:         if (errno == EINTR)     /* Alarm went off  */
 81:         {
 82:             if (tries < MAXTRIES)      /* incremented by signal handler */
 83:             {
 84:                 printf("timed out, %d more tries...\n", MAXTRIES-tries);
 85:                 if (sendto(sock, echoString, echoStringLen, 0, (struct sockaddr *)
 86:                            &echoServAddr, sizeof(echoServAddr)) != echoStringLen)
 87:                     DieWithError("sendto() failed");
 88:                 alarm(TIMEOUT_SECS);
 89:             }
 90:             else
 91:                 DieWithError("No Response");
 92:         }
 93:         else
 94:             DieWithError("recvfrom() failed");
 95: 
 96:     /* recvfrom() got something --  cancel the timeout */
 97:     alarm(0);
 98: 
 99:     /* null-terminate the received data */
100:     echoBuffer[respStringLen] = '\0';
101:     printf("Received: %s\n", echoBuffer);    /* Print the received data */
102: 
103:     close(sock);
104:     exit(0);
105: }
106: 
107: void CatchAlarm(int ignored)     /* Handler for SIGALRM */
108: {
109:     tries += 1;
110: }

I/O模型

阻塞与非阻塞I/O

在进行读操作时,如果缓冲区中没有数据,相应的函数就不会返回,线程会一直等待直到缓冲区中有数据;在进行写操作时,如果缓冲区已满,相应的函数也不会返回,一直等待到其他进程从缓冲区中读了一定的数据使得缓冲区非空。这种I/O模式就是阻塞式I/O。socket中的read()/readfrom()、send()/sendto()、accept()和connect()默认情况下都是阻塞式的。阻塞模式下,accept()在没有客户端发出连接时会一直等待,connect()会在建立连接成功前一直等到(直到超时)。

在阻塞模式下,在I/O操作完成前,函数会 一直等待 而不会 立即返回 ,且该函数所在的线程会被阻塞。

非阻塞模式时,如果当前的I/O操作暂时不能完成,非得让线程等待时,线程就不等待,函数直接返回一个错误(EAGAIN/EWOULDBLOCK,connect()返回EINPROGRESS)。这种情况下,需要应用程序靠轮询来检查操作是否可用,这对CPU时间是极大的浪费。

socket的各个函数默认都是阻塞式的,使用fcntl()函数可以改变默认行为,使它们变成非阻塞式的。

1: int fcntl(int socket, int command, long arg);

这个函数可以获取/设置套接字的flags。第一个参数就是相应的套接字描述符,第二个参数为 F_GETFLF_SETFL ,前者用来获取flags,或者用来设置flags。而非阻塞的flag值为 O_NONBLOCK

可以这样将socket设置为非阻塞:

1: fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL) | O_NONBLOCK);

同步与异步I/O

同步I/O模式下,操作必须是按部就班的,做完一件事情才能做下一件事情。从这个意义上来说,阻塞与非阻塞都可以算是同步的,或者说,只有在同步I/O模式下,阻塞与非阻塞的区分才有意义。

对于异步I/O来说,应用程序不需要在做完一件事情前一直等待或者轮询,而是在一件事情没做好时去做别的事情,当之前的某个操作完成时, 由内核来通知应用程序 。在异步I/O模式下,应用程序是不会阻塞在某个I/O操作中的,因此在异步I/O模式下讨论阻塞与非阻塞也是没有意义的。

异步I/O通过传递信号 SIGIO 来工作。

在socket编程中使用异步模型,首先要定义对信号 SIGIO 的处理方法并和信号 SIGIO 绑定;其次要将套接字设置为异步和非阻塞模式(使用fcntl());最后还要将当前的进程设置为套接字的“拥有者”(使用fcntl())。

下面是一个使用异步I/O的UDP Server示例:

  1: #include <stdio.h>      /* for printf() and fprintf() */
  2: #include <sys/socket.h> /* for socket(), bind, and connect() */
  3: #include <arpa/inet.h>  /* for sockaddr_in and inet_ntoa() */
  4: #include <stdlib.h>     /* for atoi() and exit() */
  5: #include <string.h>     /* for memset() */
  6: #include <unistd.h>     /* for close() and getpid() */
  7: #include <fcntl.h>      /* for fcntl() */
  8: #include <sys/file.h>   /* for O_NONBLOCK and FASYNC */
  9: #include <signal.h>     /* for signal() and SIGALRM */
 10: #include <errno.h>      /* for errno */
 11: 
 12: #define ECHOMAX 255     /* Longest string to echo */
 13: 
 14: void DieWithError(char *errorMessage);  /* Error handling function */
 15: void UseIdleTime();                     /* Function to use idle time */
 16: void SIGIOHandler(int signalType);      /* Function to handle SIGIO */
 17: 
 18: int sock;                        /* Socket -- GLOBAL for signal handler */
 19: 
 20: int main(int argc, char *argv[])
 21: {
 22:     struct sockaddr_in echoServAddr; /* Server address */
 23:     unsigned short echoServPort;     /* Server port */
 24:     struct sigaction handler;        /* Signal handling action definition */
 25: 
 26:     /* Test for correct number of parameters */
 27:     if (argc != 2)
 28:     {
 29:         fprintf(stderr,"Usage:  %s <SERVER PORT>\n", argv[0]);
 30:         exit(1);
 31:     }
 32: 
 33:     echoServPort = atoi(argv[1]);  /* First arg:  local port */
 34: 
 35:     /* Create socket for sending/receiving datagrams */
 36:     if ((sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0)
 37:         DieWithError("socket() failed");
 38: 
 39:     /* Set up the server address structure */
 40:     memset(&echoServAddr, 0, sizeof(echoServAddr));   /* Zero out structure */
 41:     echoServAddr.sin_family = AF_INET;                /* Internet family */
 42:     echoServAddr.sin_addr.s_addr = htonl(INADDR_ANY); /* Any incoming interface */
 43:     echoServAddr.sin_port = htons(echoServPort);      /* Port */
 44: 
 45:     /* Bind to the local address */
 46:     if (bind(sock, (struct sockaddr *) &echoServAddr, sizeof(echoServAddr)) < 0)
 47:         DieWithError("bind() failed");
 48: 
 49:     /* Set signal handler for SIGIO */
 50:     handler.sa_handler = SIGIOHandler;
 51:     /* Create mask that mask all signals */
 52:     if (sigfillset(&handler.sa_mask) < 0)
 53:         DieWithError("sigfillset() failed");
 54:     /* No flags */
 55:     handler.sa_flags = 0;
 56: 
 57:     if (sigaction(SIGIO, &handler, 0) < 0)
 58:         DieWithError("sigaction() failed for SIGIO");
 59: 
 60:     /* We must own the socket to receive the SIGIO message */
 61:     if (fcntl(sock, F_SETOWN, getpid()) < 0)
 62:         DieWithError("Unable to set process owner to us");
 63: 
 64:     /* Arrange for nonblocking I/O and SIGIO delivery */
 65:     if (fcntl(sock, F_SETFL, O_NONBLOCK | FASYNC) < 0)
 66:         DieWithError("Unable to put client sock into non-blocking/async mode");
 67: 
 68:     /* Go off and do real work; echoing happens in the background */
 69: 
 70:     for (;;)
 71:         UseIdleTime();
 72: 
 73:     /* NOTREACHED */
 74: }
 75: 
 76: void UseIdleTime()
 77: {
 78:     printf(".\n");
 79:     sleep(3);     /* 3 seconds of activity */
 80: }
 81: 
 82: void SIGIOHandler(int signalType)
 83: {
 84:     struct sockaddr_in echoClntAddr;  /* Address of datagram source */
 85:     unsigned int clntLen;             /* Address length */
 86:     int recvMsgSize;                  /* Size of datagram */
 87:     char echoBuffer[ECHOMAX];         /* Datagram buffer */
 88: 
 89:     do  /* As long as there is input... */
 90:     {
 91:         /* Set the size of the in-out parameter */
 92:         clntLen = sizeof(echoClntAddr);
 93: 
 94:         if ((recvMsgSize = recvfrom(sock, echoBuffer, ECHOMAX, 0,
 95:                                     (struct sockaddr *) &echoClntAddr, &clntLen)) < 0)
 96:         {
 97:             /* Only acceptable error: recvfrom() would have blocked */
 98:             if (errno != EWOULDBLOCK)
 99:                 DieWithError("recvfrom() failed");
100:         }
101:         else
102:         {
103:             printf("Handling client %s\n", inet_ntoa(echoClntAddr.sin_addr));
104: 
105:             if (sendto(sock, echoBuffer, recvMsgSize, 0, (struct sockaddr *)
106:                        &echoClntAddr, sizeof(echoClntAddr)) != recvMsgSize)
107:                 DieWithError("sendto() failed");
108:         }
109:     }  while (recvMsgSize >= 0);
110:     /* Nothing left to receive */
111: }

多任务处理

迭代服务器与并发服务器

一个最简单的服务器实现,是一次只能处理一个客户端请求的,且只能顺序处理客户端的请求,这种最简单的服务器被称为“迭代服务器(iterative servers)”。

这种服务器无法充分利用多核CPU,不适合处理时间较长的连接,否则对于其他客户端而言,等待的时间将无法忍受。

另一种服务器模式则能够“同时”处理多个客户端的连接请求,这种服务器被称为“并发服务器(concurrent servers)”。并发服务器的模型有:一客户端一进程、一客户端一线程以及约束多任务。

一客户端一进程

当检查到客户端请求时,服务器创建一个子进程来处理连接请求。在Unix/Linux下,通常是调用fork()来做这件事情。要注意的是,在父进程中应该调用waitpid()来为子进程“收尸”,并且在父进程中应该关闭子套接字(accept()得到的套接字),在子进程中应该关闭监听套接字。

有时候也需要在子进程中获取连接的信息,如IP地址以及端口号,而采用现在所说的模型时,子进程是不能直接得到这些信息的(accept()只在父进程中执行),这时可以使用以下两个函数:

1: int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
2: int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

前者用来获取和指定套接字连接的另一端的地址信息,后者用来获取本地的地址信息。

一客户端一线程

为每个客户端连接请求建立子进程具有编码简单的优点,但对资源的消耗比较大。从这个角度来考虑,使用线程可以节省资源、提高效率。不过线程模式会使编程难度增大,因为多线程中必须要考虑数据同步和互斥的情况。

不管是之前的为每个客户端请求建立一个子进程的模式还是现在说的为每个客户端请求建立线程的模式,它们都更适用于少量客户端的情况,因为在同一个系统中,进程数和线程数都是有限的,而且在不同进程/线程之间切换的开销也会随着进程数/线程数的增大而升高。

进程池和线程池

所谓的“约束多任务”就是为了解决上面两种模式的问题而建立的模型。这种模型的思想是,预先建立若干个子进程/子线程,然后在多个子进程/子线程中来处理多个客户端连接请求。

注意该模式和之前两种模式的区别。拿多进程模式来说,是先accept()得到客户端连接请求后,再建立子进程来进行处理;而进程池,则是先创建子进程,然后在子进程能够中监听客户端的连接请求。

多路复用

多路复用也是socket编程中常用到的一种模型。

在最简单的socket模型中,一旦建立连接,随后就会进行数据的接收/发送,而当缓冲区中为空/已满时,将会阻塞。在只有单个连接的时候,这样的简单模型也是可用的。但当实际应用需要同时处理大量连接时,一个阻塞的连接将会导致来自其他地址的连接进入等待状态。

之前提到的几种方式也可以解决这个问题,如使用信号机制或多任务机制。除此以外还可以使用“多路复用”技术来解决这个问题。

使用select()可以做到这件事情。使用select()时,select()检查给定的一个socket描述符集合,当没有可用的socket时,在select()中阻塞(而不是在真正的I/O操作中阻塞),直到发现可用的socket时,select()就返回。程序这时就可以对那些可用的socket进行操作。

select()的原型为:

1: int select(int nfds, fd_set *readfds, fd_set *writefds,
2:            fd_set *exceptfds, struct timeval *timeout);

其中中间三个参数都是socket描述符的集合。readfds指向的socket描述符集合被检查是否可读,writefds指向的socket描述符集合被检查是否科协,exceptfds指向的集合被检查是否有特殊情况发生。

系统提供了四个宏对socket描述符集合进行操作,它们是:

1: void FD_ZERO(fd_set *set);         /* 将集合清空 */
2: void FD_CLR(int fd, fd_set *set);  /* 将socket描述符从集合中移除 */
3: void FD_SET(int fd, fd_set *set);  /* 将socket描述符加入集合 */
4: int FD_ISSET(int fd, fd_set *set); /* 判断socket描述符是否集合成员 */

第一个参数必须是所有要监视的socket描述符中最大值加1后得到的值,注意,是 所有 ,即三个socket描述符集合都要考虑到。

最后一个参数设定了select()等待的时间上限,即在等待时间内所监视的socket描述符集合中都没有可用的,则判定为超时,select()在这种情况下会直接返回而不再等待。

结构timeval如下:

1: struct timeval {
2:     time_t tv_sec;              /* */
3:     time_t tv_usec;             /* 微秒 */
4: };

如果指定时间内没有任何socket可用,select()返回0。否则返回可用的socket数量,并且在相应的socket描述符集合中 只保留可用的socket描述符 ,因此在select()检测到可用的socket并返回后,可以用宏 FD_ISSET 来检查具体有哪些socket可用。

如果发生错误,select()返回-1并设置错误变量 errno

因为socket描述符实际上是文件描述符,因此采用多路复用技术时,还可以检测有无键盘输入。

select()有其自身缺陷,如它采用主动轮询的方式,逐个检查集合中的socket描述符,直到发现可用的socket,因此效率不高;且select()可以监视的连接数是有限的,因为系统的文件描述符是有限的(通常是1024)。

单播、多播和广播

  • 单播

    单播(unicast)是一对一的通信,也就是点对点通信。在这之前提到及实现的socket连接都是单播的。

  • 广播

    有些时候需要同时和多个地址进行通信,如果采用单播的方式,那就需要建立多条连接,并且在多条连接里发送多份同样的数据,这无疑是对带宽的浪费。通过使用UDP,可以将发送数据到多个接收者那里的工作交给网络,其中广播(broadcast)是常用的一种方式,即将数据发送到同一网络下的所有地址——一般都是指子网广播,全网广播理论上来说也可以,但是路由器通常会过滤掉这种全网广播。

    由于TCP连接必须指定连接双方的具体地址,广播(和多播)都只能通过UDP来实现。

    通过UDP实现广播,和实现单播只有几处不同。

    首先,在具体实现中,要调用setsockopt()来将socket设置为广播属性,形式如下:

    1: int braodcast = 1;
    2: setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST,
    3:            (void *)&broadcast, sizeof(broadcast));
    

    其次,要设置信息接收方的IP地址为广播地址。

    对于广播信息的接收方,除了将要接收的地址设置为 INADDR_ANY 外,与单播的UDP接收实现并无太多。即广播信息的接收方,大致是这样的:

     1: int sockfd;
     2: struct sockaddr_in broadcast_addr;
     3: unsigned int port=8888;
     4: char msg[256];
     5: int msg_len;
     6: 
     7: sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
     8: 
     9: bzero(&broadcast_addr, sizeof(broadcast_addr));
    10: broadcast_addr.sin_family = AF_INET;
    11: broadcast_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    12: broadcast_addr.sin_port = htons(port);
    13: 
    14: bind(sockfd, (struct sockaddr *)&broadcast_addr,
    15:      sizeof(broadcast_addr));
    16: 
    17: msg_len = recvfrom(sockfd, msg, sizeof(msg), 0, NULL, 0);
    
  • 多播

    多播(Mluticast),也被称作“组播”。

    组播也单播类似,区别在于地址形式。发送组播信息必须使用组播地址,即以”1110“作为网络标识符的D类地址

    类似广播,要实现组播,也要调用setsockopt():

    1: int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    2: unsigned char ttl;
    3: setsockfd(sockfd, IPPROTO_IP, IP_MULTICAST_TTL, (void *)&ttl);
    

    和广播的不同之处在于,组播不需要将socket设置为组播属性,但组播需要设置TTL。

    在接收方,稍微有一点复杂。所谓组播地址是一些共享地址,如果要能够接收组播信息,接收方必须得加入组播组。因此组播信息的接收方的实现大致如下:

    首先创建的socket也必须是UDP socket:

    1: int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    

    然后要绑定组播端口,并设置为接收所有发送到该端口的信息:

    1: struct sockaddr_in multi_addr;
    2: multi_addr.sin_family = AF_INET;
    3: multi_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    4: multi_addr.sin_port = htons(PORT);
    5: bind(sockfd, (struct sockaddr *)&multi_addr, sizeof(multi_addr));
    

    还要加入组播组:

    1: struct ip_mreq multi_request;
    2: multi_request.imr_multiaddr.s_addr = inet_addr(multi_addr); /* 与发送方一致 */
    3: multi_request.imr_interface.s_addr = htonl(INADDR_ANY);
    4: setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, /* 加入组播组 */
    5:            (void *)&multi_request, sizeof(multi_request));
    

    这之后如果不出错就可以进行信息的接收了:

    1: int msg[256];
    2: int msg_len;
    3: 
    4: msg_len = recvfrom(sockfd, msg, sizeof(msg), 0, NULL, 0);