TCP/IP详解卷1:第二章(链路层)

最后更新于:2022-04-01 14:49:16

## 1. 以太网和IEEE 802封装 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b31805a5.jpg) ## 2. SLIP:串行线路IP    SLIP的全称是serial line IP。它是一种在串行线路上对IP数据报进行封装的简单形式。下列规格描述SLIP的帧格式: 1) IP数据报以一个称作END(0xc0)的特殊字符结束。同时,为了防止数据报到来之前的线路噪声被当做数据报内容,大多数实现在数据报开始处也传一个END字符(如果有线路噪声,那么END字符将结束这份错误的报文。这样当前的报文得以正确的传输,而前一个错误报文上交给上层后,会发现其内容毫无意义而被丢弃) 2)如果IP报文中某个字符为END,那么就连续传输两个字节0xdb和0xdc来取代它。0xdb这个特殊的字符被称作SLIP的ESC字符,但是不同于ASCII的ESC字符 3) 如果IP报文中某个字符为SLIP的ESC字符,那么就要连续传输两个字节0xdb和0xdd来取代它。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b31a1e48.jpg)    但是SLIP有以下缺陷: 1) 每一端必须知道对方的IP地址。没有办法把本端的IP地址通知给另一端。 2) 数据帧中没有类型字段。如果一条串行线路用于SLIP,那么它不能同时使用其他协议。 3) SLIP没有在数据帧中加上校验和。 ## 3. PPP:点对点协议    PPP,点对点协议修改了SLIP协议中的所有缺陷。PPP包括以下三个部分: 1) 在串行链路上封装IP数据报的方法。 2) 建立,配置及测试数据链路的链路控制协议。 3) 针对不同网络层协议的网络控制协议体系。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b31b6289.jpg)    具体实现过程如下: 1) 当遇到字符0x7e时,需连续传送两个字符:0x7d和0x5e,以实现标志字符的转义。 2) 当遇到转义字符0x7d时,需连续传送两个字符:0x7d和0x5d,以实现转义字符的转义。 3) 默认情况下,如果字符的值小于0x20,一般都要进行转义。    PPP比SLIP的优势如下: 1) PPP支持在单根串行线路上运行多种协议,不只是IP协议 2) 每一帧都有循环冗余校验 3) 通信双方可以进行IP地址的动态协商(使用IP网络控制协议) 4) 与CSLIP类似,对TCP和IP报文首部进行压缩 5) 链路控制协议可以对多个数据链路选项进行设置。 ## 4. 环回接口    环回接口:允许运行在同一台主机上的客户程序和服务器程序通过TCP/IP进行通信。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b31ceaef.jpg) 1) 传给环回地址(一般为127.0.0.1)的任何数据均作为IP输入 2) 传给广播地址或多播地址的数据报复制一份传给环回接口,然后送到以太网上。 3) 任何传给该主机IP地址的数据均送到环回接口。
';

TCP/IP详解卷1:第一章(概述)

最后更新于:2022-04-01 14:49:14

## 1. 分层    TCP/IP通常被认为是一个四层协议系统。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b30e687c.jpg) 1)链路层:有时也被称作数据链路层或网络接口层,通常包括操作系统中的设备驱动程序和计算机中对应的网络接口卡。它们一起处理与电缆的物理接口细节。 2)网络层:有时也称作互联网层,处理分组在网络中的活动,例如分组的选路。在TCP/IP协议族中,网络层协议包括IP协议(网际协议),ICMP协议(internet互联网控制保温协议),以及IGMP协议(internet组管理协议) 3)运输层主要为两台主机上的应用程序提供端到端的通信。在TCP/IP协议族中,有两个互不相同的传输协议:TCP和UDP 4) 应用层负责处理特定的应用程序细节。 ### 1. 包含两个网络的互连网    下图为包含两个网络的互连网:一个以太网和一个令牌环网,通过一个路由器互相连接。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b31073ef.jpg)    应用层和运输层使用端到端协议,只有端系统需要这两层协议。但是,网络层(IP)提供的却是逐跳协议,两个端系统和每个中间系统都要使用它。    TCP/IP协议族中,网络层IP提供的是一种不可靠服务。也就是说,它只是尽可能快的把分组从源点送到目的结点,但是并不提供任何可靠性保证。而另一方面,TCP在不可靠的IP层上提供了一个可靠的运输层。为了提供这种可靠的服务,TCP采用了超时重传,发送和接收到端的确认分组等机制。 ## 2. TCP/IP的分层 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b3122809.jpg) 1) TCP提供一种可靠的运输层服务。 2) UDP为应用程序发送和接收数据报。一个数据报是指从发送方传输到接收方的一个信息单元,但是UDP是不可靠的。 3) IP是网络层上的主要协议,同时被TCP和UDP使用。TCP和UDP的每组数据都通过端系统和每个中间路由器中的IP层在互联网中进行传输。 4) ICMP是IP协议的附属协议。IP层用它来与其它主机或路由交换错误报文和其它重要信息。 5) IGMP是internet组管理协议。它用来把一个UDP数据报多播到多个主机。 6) ARP(地址解析协议)和RARP(逆地址解析协议)是某些网络接口使用的特殊协议,用来转换IP层和网络接口层使用的地址。 ## 3. 封装与分用 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b313a9b7.jpg) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b31606d6.jpg)
';

UNP卷1:第二十章(广播)

最后更新于:2022-04-01 14:49:12

## 1. 单播和广播的比较 ### UDP数据报单播示例: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b30a2305.jpg) 1. 通过ARP将IP地址转换为目的以太网地址:00:0a:95:79:bc:b4 2. 中间主机的以太网接口看到该帧后把它的目的以太网地址与自己的以太网地址(00:04:ac:17:bf:38)进行比较。既然它们不一致,该接口于是忽略这个帧。可见单播帧不会对该主机造成任何额外开销,因为忽略它们的是接口而不是主机。 3. 右侧主机的以太网接口也看到这个帧,当它比较该帧的目的以太网地址和自己的以太网地址时,会发现它们相同。该接口于是读入整个帧,读入完毕后可能产生一个硬件中断,致使相应设备驱动程序从接口内存中读取该帧。既然该帧类型为0x8000,该帧承载的分组于是被置于IP的输入队列。    单播IP数据报仅通过目的IP地址指定的单个主机接收。子网上的其他主机都不受任何影响。 ### UDP数据报广播示例 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b30c4b93.jpg) 1. 左侧主机发送该数据报时候发现,IP地址定向为广播地址,则将此IP映射为:ff:ff:ff:ff:ff:ff的以太网地址。这个地址使得该子网上的每一个以太网接口都接收该帧。 2. 右侧的那个主机把该UDP数据报传递给绑定端口520的应用进程。一个应用进程无需就为接收广播UDP数据报而进行任何特殊处理:它只需要创建一个UDP套接字,并把应用的端口号捆绑到其上。 3. 然而中间的那个主机没有任何应用进程绑定UDP端口520.该主机的UDP代码于是丢弃这个已收取的数据报。该主机绝不能发送一个ICMP端口不可达消息,因为这么做可能产生广播风暴,即子网上大量主机几乎同时产生一个响应,导致网络在这段时间内不可用。 4. 左侧主机的数据报也被传递给自己。 ### 广播存在的根本问题:    子网上未参加相应广播应用的所有主机也不得不沿着协议栈一路向上完整的处理收取的UDP广播数据报,直到该数据报历经UDP层时被丢弃为止。另外,子网上所有非IP的主机也不得不在数据链路层街搜完整的帧,然后在丢弃它。 ### 广播实例1: tserv.c: ~~~ #include <sys/socket.h> #include <stdio.h> #include <netinet/in.h> #include <time.h> #define MAXLINE 1024 #define SA struct sockaddr int main(int argc, char **argv) { struct sockaddr_in srvaddr; int sockfd, on = 1; int num, i; time_t ticks; char mesg[MAXLINE + 1]; if (argc != 3){ printf("usage:%s<ip address><port>\n", argv[0]); exit(0); } sockfd = socket(AF_INET, SOCK_DGRAM, 0); setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(int)); bzero(&srvaddr, sizeof(srvaddr)); srvaddr.sin_family = AF_INET; if (inet_pton(AF_INET, argv[1], &srvaddr.sin_addr) <= 0){ printf("wrong dest ip address\n"); exit(0); } srvaddr.sin_port = htons(atoi(argv[2])); for ( ; ; ){ ticks = time(NULL); snprintf(mesg, sizeof(mesg), "%.24s\r\n", ctime(&ticks)); sendto(sockfd, mesg, strlen(mesg), 0, (SA *)&srvaddr, sizeof(srvaddr)); sleep(5); } return 0; } ~~~ tcli.c: ~~~ #include <sys/socket.h> #include <stdio.h> #include <netinet/in.h> #include <sys/types.h> #define MAXLINE 1024 #define SA struct sockaddr int main(int argc, char **argv) { struct sockaddr_in cliaddr; int sockfd, n, opt; char mesg[MAXLINE + 1]; if (argc != 2){ printf("usage:%s<port>\n", argv[1]); exit(0); } sockfd = socket(AF_INET, SOCK_DGRAM, 0); bzero(&cliaddr, sizeof(cliaddr)); cliaddr.sin_port = htons(atoi(argv[1])); cliaddr.sin_addr.s_addr = htonl(INADDR_ANY); opt = SO_REUSEADDR; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); bind(sockfd, (SA *)&cliaddr, sizeof(cliaddr)); n = read(sockfd, mesg, MAXLINE); if (n > 0){ mesg[n] = 0; printf("%s\n", mesg); } return 0; } ~~~ 程序输出: 服务端: ~~~ leichaojian@ThinkPad-T430i:~$ ./tserv 192.168.0.255 9876 ~~~ 客户端1: ~~~ leichaojian@ThinkPad-T430i:~$ ./tcli 9876 Tue Oct 14 20:40:43 2014 ~~~ 客户端2: ~~~ leichaojian@ThinkPad-T430i:~$ ./tcli 9876 Tue Oct 14 20:40:53 2014 ~~~ ### 广播实例2: bcastsrv.c: ~~~ #include <sys/socket.h> #include <signal.h> #include <stdio.h> #include <netinet/in.h> #include <time.h> #include <errno.h> extern int errno; #define MAXLINE 1024 #define SA struct sockaddr static void recvfrom_alarm(int signo); void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen); int main(int argc, char **argv) { struct sockaddr_in srvaddr; int sockfd, on = 1; int n; char mesg[MAXLINE + 1]; if (argc != 3){ printf("usage:%s<ip addr><port>\n", argv[0]); exit(0); } sockfd = socket(AF_INET, SOCK_DGRAM, 0); bzero(&srvaddr,sizeof(srvaddr)); srvaddr.sin_family = AF_INET; inet_pton(AF_INET, argv[1], &srvaddr.sin_addr); srvaddr.sin_port = htons(atoi(argv[2])); dg_cli(stdin, sockfd, (SA *)&srvaddr, sizeof(srvaddr)); return 0; } static void recvfrom_alarm(int signo) { return; } void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen) { int n; const int on = 1; char sendline[MAXLINE], recvline[MAXLINE + 1]; socklen_t len; struct sockaddr *preply_addr; preply_addr = malloc(servlen); setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)); signal(SIGALRM, recvfrom_alarm); while (fgets(sendline, MAXLINE, fp) != NULL){ sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen); // alarm(3); // for ( ; ; ){ len = servlen; n = recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len); if (n < 0){ if (errno == EINTR) break; else printf("recvfrom error\n"); } else{ recvline[n] = 0; inet_ntop(AF_INET, &preply_addr, sendline, sizeof(sendline)); printf("from %s:%s\n", sendline, recvline); } // } } free(preply_addr); } ~~~ bcastcli.c: ~~~ #include <stdio.h> #include <sys/socket.h> #include <sys/types.h> #include <time.h> #include <netinet/in.h> #define MAXLINE 1024 #define SA struct sockaddr int main(int argc, char **argv) { struct sockaddr_in cliaddr; int sockfd, n, len; char mesg[MAXLINE]; time_t ticks; if (argc != 2){ printf("usage:%s<port>\n", argv[0]); exit(0); } sockfd = socket(AF_INET, SOCK_DGRAM, 0); bzero(&cliaddr, sizeof(cliaddr)); cliaddr.sin_port = htons(atoi(argv[1])); cliaddr.sin_addr.s_addr = htonl(INADDR_ANY); cliaddr.sin_family = AF_INET; bind(sockfd, (SA *)&cliaddr, sizeof(cliaddr)); for ( ; ; ){ len = sizeof(cliaddr); n = recvfrom(sockfd, mesg, MAXLINE, 0, (SA *)&cliaddr, &len); if (n < 0){ // sleep(5); continue; } mesg[n] = 0; printf("recv: %s\n", mesg); ticks = time(NULL); snprintf(mesg, sizeof(mesg), "%.24s", ctime(&ticks)); sendto(sockfd, mesg, 2424, 0, (SA *)&cliaddr, len); } return 0; } ~~~ 服务端: ~~~ leichaojian@ThinkPad-T430i:~$ ./bcastsrv 192.168.0.255 9876 hello from 16.224.187.0:Tue Oct 14 21:45:38 2014 what from 16.224.187.0:Tue Oct 14 21:45:43 2014 ha from 16.224.187.0:Tue Oct 14 21:45:47 2014 ~~~ 客户端: ~~~ leichaojian@ThinkPad-T430i:~$ ./bcastcli 9876 recv: hello recv: what recv: ha ~~~
';

UNP卷1:第十五章(unix域协议)

最后更新于:2022-04-01 14:49:09

## 1. 概述  unix域协议并不是一个实际的协议族,而是在单个主机上执行客户/服务器通信的一种方法。unix域提供两类套接字:字节流套接字(类似TCP)和数据报套接字(类似UDP)。使用unix域协议有如下的优势: (1)unix域套接字往往比通信两端位于同一个主机的TCP套接字快出一倍。 (2)unix域套接字可用于在同一个主机上的不同进程之间传递描述符。 (3)unix域套接字较新的实现把客户的凭证(用户ID和组ID)提供给服务器,从而能够提供额外的安全检查措施。  unix域中用于标识客户和服务器的协议地址是普通文件系统中的路径名。 ### 1) unix域套接字地址结构 struct sockaddr_un{  sa_family_t    sun_family;        /*AF_LOCAL*/  char        sun_path[104];        /*null-terminated pathname*/ }; ### 2) unix域套接字的bind调用 ~~~ #include <sys/socket.h> #include <sys/un.h> #include <stdio.h> #include <netdb.h> #define SA struct sockaddr int main(int argc, char **argv) { int sockfd; socklen_t len; struct sockaddr_un addr1, addr2; if (argc != 2){ printf("argument should be 2\n"); exit(1); } sockfd = socket(AF_LOCAL, SOCK_STREAM, 0); unlink(argv[1]); bzero(&addr1, sizeof(addr1)); addr1.sun_family = AF_LOCAL; strncpy(addr1.sun_path, argv[1], sizeof(addr1.sun_path) - 1); bind(sockfd, (SA *)&addr1, SUN_LEN(&addr1)); len = sizeof(addr2); getsockname(sockfd, (SA *)&addr2, &len); printf("bound name = %s, returned len = %d\n", addr2.sun_path, len); return 0; } ~~~ 程序输出: ~~~ leichaojian@ThinkPad-T430i:~$ ./unixbind /tmp/moose bound name = /tmp/moose, returned len = 13 leichaojian@ThinkPad-T430i:~$ ll /tmp/moose srwxrwxr-x 1 leichaojian leichaojian 0 10月 8 22:18 /tmp/moose= ~~~ ### 3)套接字函数 (1)由bind创建的路径名默认访问权限应为0777,并按照当前umask值进行修正。 (2)与unix域套接字关联的路径名应该是一个绝对路径名,而不是一个相对路径名。避免使用后者的原因是它的解析依赖于调用者的当前工作目录。也就是说,要是服务器捆绑一个相对路径名,客户就得在与服务器相同的目录中(或者必须知道这个目录)才能成功调用connect或sendto。 (3)在connect调用中指定的路径名必须是一个当前绑定在某个打开的unix域套接字上的路径名,而且它们的套接字类型(字节流或数据报)也必须一致。出错条件包括:(a)该路径名已存在却不是一个套接字(unlink来删除已存在文件);(b)该路径名已存在且是一个套接字,不过没有与之关联的打开的描述符;(c)该路径名已存在且是一个打开的套接字,不过类型不符。 (4)调用connect连接一个unix域套接字涉及的权限测试等同于调用open以只写方式访问相应的路径名。 (5)unix域字节流套接字类似TCP套接字:它们都为进程提供一个无记录边界的字节流接口。 (6)如果对于某个unix域字节流套接字的connect调用发现这个监听套接字的队列已满,调用就立即返回一个ECONNREFUSED错误。 (7)unix域数据报套接字类似于UDP套接字:它们都提供一个保留记录边界的不可靠的数据报服务。 (8)在一个未绑定的unix域套接字上发送数据报不会自动给这个套接字捆绑一个路径名,connect也一样。 ## 2. unix域字节流客户/服务器程序 服务端: ~~~ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/un.h> #include <signal.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #define SA struct sockaddr #define MAXLINE 1024 #define UNIXSTR_PATH "/tmp/unix.str" extern int errno; void sig_chld(int); void str_echo(int sockfd); int main(int argc, char **argv) { int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_un cliaddr, servaddr; listenfd = socket(AF_LOCAL, SOCK_STREAM, 0); unlink(UNIXSTR_PATH); bzero(&servaddr, sizeof(servaddr)); servaddr.sun_family = AF_LOCAL; strcpy(servaddr.sun_path, UNIXSTR_PATH); bind(listenfd, (SA *)&servaddr, sizeof(servaddr)); listen(listenfd, 5); signal(SIGCHLD, sig_chld); for ( ; ; ){ clilen = sizeof(cliaddr); if ((connfd = accept(listenfd, (SA *)&cliaddr, &clilen)) < 0){ if (errno == EINTR) continue; else{ printf("accept error\n"); exit(0); } } if ((childpid = fork()) == 0){ close(listenfd); str_echo(connfd); exit(0); } close(connfd); } } void str_echo(int sockfd) { char recvline[MAXLINE]; int n; while ((n = read(sockfd, recvline, MAXLINE)) > 0){ recvline[n] = '\0'; write(sockfd, recvline, n); } } void sig_chld(int signo) { pid_t pid; int stat; while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) printf("child %d terminated\n", pid); return; } ~~~ 客户端: ~~~ #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <sys/un.h> #define SA struct sockaddr #define UNIXSTR_PATH "/tmp/unix.str" #define MAXLINE 1024 void str_cli(FILE *fd, int sockfd); int main(int argc, char **argv) { int sockfd; struct sockaddr_un servaddr; sockfd = socket(AF_LOCAL, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sun_family = AF_LOCAL; strcpy(servaddr.sun_path, UNIXSTR_PATH); connect(sockfd, (SA *)&servaddr, sizeof(servaddr)); str_cli(stdin, sockfd); exit(0); } void str_cli(FILE *fd, int sockfd) { int n; int recvline[MAXLINE], sendline[MAXLINE]; while (fgets(sendline, MAXLINE, fd) != NULL){ write(sockfd, sendline, strlen(sendline)); if ((n = read(sockfd, recvline, MAXLINE)) > 0){ fputs(recvline, stdout); } } } ~~~ 程序输出: 服务端: ~~~ leichaojian@ThinkPad-T430i:~$ ./unixstrserv child 12698 terminated child 12700 terminated ~~~ 客户端: ~~~ leichaojian@ThinkPad-T430i:~$ ./unixstrcli hello world hello world ^C leichaojian@ThinkPad-T430i:~$ ./unixstrcli what what ^C ~~~ ## 3. 描述符传递    当考虑从一个进程到另一个进程传递打开的描述符时,我们通常会想到: 1)fork调用返回之后,子进程共享父进程的所有打开的描述符。 2)exec调用执行之后,所有描述符通常保持打开状态不变。    在第一个例子中,进程先打开一个描述符,再调用fork,然后父进程关闭这个描述符,子进程则处理这个描述符。这样一个打开的描述符就从父进程传递到子进程。那如何从子进程传递描述符到父进程呢?    当前的unix系统提供了用于从一个进程向任一其他进程传递任一打开的描述符的方法。这种技术要求首先在这两个进程之间创建一个unix域套接字,然后使用sendmsg跨这个套接字发送一个特殊信息。这个消息由内核来专门处理,会把打开的描述符从发送进程传递到接收进程。    在两个进程之间传递描述符涉及的步骤如下: 1) 创建一个字节流或数据报的unix域套接字    如果目标是fork一个子进程,让子进程打开待传递的描述符,再把它传递回父进程,那么父进程可以预先调用socketpair创建一个可用于父子进程之间交换描述符的流管道。    如果进程之间没有亲缘关系,那么服务器必须创建一个unix域字节流套接字,bind一个路径名到该套接字,以允许客户进程connect到该套接字。然后客户可以向服务器发送一个打开某个描述符的请求,服务器再把该描述符通过unix域套接字传递回客户。 2) 发送进程通过调用返回描述符的任一unix函数打开一个描述符,这些函数的例子有open,pipe,mkfifo,socket和accept,可以在进程之间传递的描述符不限类型。 3) 发送进程创建一个msghdr结构,其中含有待传递的描述符。 4) 接收进程调用recvmsg在来自步骤1的unix域套接字上接收这个描述符。 程序mycat.c如下: ~~~ #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <sys/un.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> extern int errno; #define BUFFSIZE 4096 int my_open(const char *pathname, int mode); ssize_t read_fd(int fd, void *tr, size_t nbytes, int *recvfd); int main(int argc, char **argv) { int fd, n; char buff[BUFFSIZE]; if (argc != 2){ printf("argument should be 2\n"); return 1; } if ((fd = my_open(argv[1], O_RDONLY)) < 0){ printf("cannot open %s\n", argv[1]); exit(1); } while ((n = read(fd, buff, BUFFSIZE)) > 0) write(STDOUT_FILENO, buff, n); return 0; } int my_open(const char *pathname, int mode) { int fd, sockfd[2], status; pid_t childpid; char c, argsockfd[10], argmode[10]; //socketpair函数创建两个随后连接起来的套接字,因为随后fork,所以实际上sockfd存储的是连接起来的父子进程 socketpair(AF_LOCAL, SOCK_STREAM, 0, sockfd); if ((childpid = fork()) == 0){ close(sockfd[0]); //因为子进程会完全复制父进程的描述符,所以要关闭父进程的描述符 snprintf(argsockfd, sizeof(argsockfd), "%d", sockfd[1]); //子进程将描述符传递给流管道父进程的一端(即sockfd[1]) snprintf(argmode, sizeof(argmode), "%d", mode); execl("./openfile", "openfile", argsockfd, pathname, argmode, (char *)NULL); printf("execl error\n"); } close(sockfd[1]); //父进程中关闭子进程的描述符(这里如果父进程关闭sockfd[1],则子进程就关闭sockfd[0],反之亦然) waitpid(childpid, &status, 0); if (WIFEXITED(status) == 0){ printf("child did not terminate\n"); exit(1); } if ((status = WEXITSTATUS(status)) == 0) read_fd(sockfd[0], &c, 1, &fd); else{ errno = status; fd = -1; } close(sockfd[0]); return (fd); return 1; } ssize_t read_fd(int fd, void *ptr, size_t nbytes, int *recvfd) { struct msghdr msg; struct iovec iov[1]; ssize_t n; #ifdef HAVE_MSGHDR_MSG_CONTROL union{ struct cmsghdr cm; char control[CMSG_SPACE(sizeof(int))]; } control_un; struct cmsghdr *cmptr; msg.msg_control = control_un.control; msg.msg_controllen = sizeof(control_un.control); #else // int newfd; // msg.msg_accrights = (caddr_t)&newfd; // msg.msg_accrightslen = sizeof(int); #endif msg.msg_name = NULL; msg.msg_namelen = 0; iov[0].iov_base = ptr; iov[0].iov_len = nbytes; msg.msg_iov = iov; msg.msg_iovlen = 1; if ((n = recvmsg(fd, &msg, 0)) <= 0) return n; #ifdef HAVE_MSGHDR_MSG_CONTROL if ((cmptr = CMSG_FIRSTHDR(&msg)) != NULL && cmptr->cmsg_len == CMSG_LEN(sizeof(int))){ if (cmptr->cmsg_level != SQL_SOCKET){ printf("control level != SOL_SOCKET\n"); exit(1); } if (cmptr->cmsg_type != SCM_RIGHTS){ printf("control type != SCM_RIGHTS\n"); exit(1); } *recvfd = *((int)*)CMSG_DATA(cmptr); } else *recvfd = -1; #else // if (msg.msg_accrightslen == sizeof(int)) // *recvfd = newfd; // else // *recvfd = -1; #endif return n; } ~~~    但是我在调试程序的时候发现,在函数my_open中并没有进入子进程,即fork()后并没有进入子进程进行执行execl函数,导致程序的失败,这到底是为什么呢?
';

UNP卷1:第十三章(守护进程和inetd超级服务器)

最后更新于:2022-04-01 14:49:07

## 1. 概述  守护进程是在后台运行且不与任何控制终端关联的进程。unix系统通常有很多守护进程在后台运行,执行不同的管理任务。  守护进程没有控制终端通常源于它们由系统初始化脚本启动。然而守护进程也可能从某个终端由用户在shell提示符下键入命令行启动,这样的守护进程必须亲自脱离与控制终端的关联,从而避免与作业控制,终端会话管理,终端产生信号等发生任何不期望的交互,也可以避免在后台运行的守护进程非预期的输出到终端。  守护进程有多种启动方法:  1.在系统启动阶段,许多守护进程由系统初始化脚本启动。这些脚本通常位于/etc目录或以/etc/rc开头的某个目录中,它们的具体位置和内容却是实现相关的。由这些脚本启动的守护进程一开始拥有超级用户权限。  有若干个网络服务器通常从这些脚本启动:inetd超级服务器,web服务器,邮件服务器(经常是sendmail)。  2. 许多网络服务器由inetd超级服务器启动。inetd自身由上一条中的某个脚本启动。inetd监听网络请求,每当有一个请求到达时,启动相应的实际服务器(telnet服务器,FTP服务器等)  3. cron守护进程按照规则定期执行一些程序,而由它启动执行的程序同样作为守护进程运行。cron自身由第一条启动方法中的某个脚本启动  4. at命令用于指定将来某个时刻的程序执行。这些程序的执行时刻到来时,通常由cron守护进程启动执行它们,因此这些程序同样作为守护进程运行。  5.守护进程还可以从用户终端或在前台或在后台启动。这么做往往是为了测试守护进程或重启因某种原因而终止了的某个守护进程。  因为守护进程没有控制终端,所以当有事发生时它们得有输出消息的某种方法可用,而这些消息既可能是普通的通告性消息,也可能是需由系统管理员处理的紧急事件消息。syslog函数是输出这些消息的标准方法,它把这些消息发送给syslogd守护进程。 ## 2. syslog函数,openlog函数和closelog函数 备注:遇到类似的函数,具体说明请查看APUE ~~~ #include <syslog.h> void syslog(int priority, const char *message,...); void openlog(const char *ident, int options, int facility); void closelog(void); ~~~ ### 1) 作为守护进程运行的协议无关时间获取服务器程序 服务器程序daytimetcpsrv.c: ~~~ #include <stdio.h> #include <netdb.h> #include <sys/socket.h> #include <time.h> #include <syslog.h> #include <string.h> #include <stdlib.h> #include <fcntl.h> #include <signal.h> #include <unistd.h> extern int errno; int daemon_proc; #define MAXLINE 1024 #define MAXFD 64 int daemon_init(const char *pname, int facility); int tcp_listen(const char *host, const char *serv, socklen_t *addrlenp); int main(int argc, char **argv) { int listenfd, connfd; socklen_t len; char buff[MAXLINE]; time_t ticks; struct sockaddr_in cliaddr; daemon_init(argv[0], 0); listenfd = tcp_listen(argv[1], argv[2], NULL); for (; ;){ len = sizeof(cliaddr); connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &len); inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)); strcat(buff, ".this is a test\n"); syslog(LOG_INFO, buff); ticks = time(NULL); snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks)); write(connfd, buff, strlen(buff)); close(connfd); } } int daemon_init(const char *pname, int facility) { int i; pid_t pid; if ((pid = fork()) < 0) return -1; else if (pid) _exit(0); if (setsid() < 0) return -1; signal(SIGHUP, SIG_IGN); if ((pid = fork()) < 0) return -1; else if (pid) _exit(0); daemon_proc = 1; chdir("/"); for (i = 0; i < MAXFD; i++) close(i); open("/dev/null", O_RDONLY); open("/dev/null", O_RDWR); open("/dev/null", O_RDWR); openlog(pname, LOG_PID, facility); } int tcp_listen(const char *host, const char *serv, socklen_t *addrlenp) { int listenfd, n; const int on = 1; struct addrinfo hints, *res, *ressave; bzero(&hints, sizeof(struct addrinfo)); hints.ai_flags = AI_PASSIVE; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; if ((n = getaddrinfo(host, serv, &hints, &res)) != 0){ printf("tcp_listen error for %s,%s:%s\n", host, serv, gai_strerror(n)); exit(1); } ressave = res; do{ listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (listenfd < 0) continue; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); if (bind(listenfd, res->ai_addr, res->ai_addrlen) == 0) break; close(listenfd); } while ((res = res->ai_next) != NULL); if (res == NULL) printf("tcp_listen error for %s,%s\n", host, serv); listen(listenfd, 5); if (addrlenp) *addrlenp = res->ai_addrlen; freeaddrinfo(ressave); return listenfd; } ~~~ 客户端程序daytimetcpcli.c: ~~~ #include <stdio.h> #include <netdb.h> #include <sys/socket.h> #define MAXLINE 1024 int tcp_connect(const char *host, const char *serv); int main(int argc, char **argv) { int sockfd, n; char recvline[MAXLINE + 1]; socklen_t len; struct sockaddr_in cliaddr; if (argc != 3){ printf("argument should be 3\n"); exit(1); } sockfd = tcp_connect(argv[1], argv[2]); len = sizeof(cliaddr); getpeername(sockfd, (struct sockaddr *)&cliaddr, len); inet_ntop(AF_INET, &cliaddr.sin_addr, recvline, sizeof(recvline)); printf("connect to %s\n", recvline); while ((n = read(sockfd, recvline, MAXLINE)) > 0){ recvline[n] = 0; fputs(recvline, stdout); } exit(0); } int tcp_connect(const char *host, const char *serv) { int sockfd, n; struct addrinfo hints, *res, *ressave; struct sockaddr_in *cliaddr; bzero(&hints, sizeof(struct addrinfo)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; if ((n = getaddrinfo(host, serv, &hints, &res)) != 0){ printf("tcp_connect error for %s,%s:%s\n", host, serv, gai_strerror(n)); exit(1); } ressave = res; do{ sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (sockfd < 0) continue; if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0) break; cliaddr = (struct sockaddr_in *)res->ai_addr; close(sockfd); } while ((res = res->ai_next) != NULL); if (res == NULL) printf("tcp_connect error for %s,%s\n", host, serv); freeaddrinfo(ressave); return sockfd; } ~~~ 程序运行如下: 服务端: ~~~ leichaojian@ThinkPad-T430i:~$ ./daytimetcpsrv ThinkPad-T430i 9878 ~~~ 客户端: ~~~ leichaojian@ThinkPad-T430i:~$ ./daytimetcpcli ThinkPad-T430i 9878 connect to 0.0.0.0 Wed Oct 8 21:07:35 2014 ~~~ 然后我们查看/var/log/syslog这个文件,通过查找字符串“this is a test”,发现如下的语句: ~~~ Oct 8 21:07:35 ThinkPad-T430i ./daytimetcpsrv[10528]: 127.0.0.1.this is a test ~~~ ### 2) 对daemon_init函数的分析 ### (1)fork 用于产生子进程 ### (2)setsid setsid用于创建一个新的回话。当前进程变为新会话的会话头进程以及新进程组的进程组头进程,从而不再有控制终端。 ### (3)忽略SIGHUP信号并再次fork 忽略SIGHUP信号并再次调用fork。该函数返回时,父进程实际上是上一次调用fork产生的子进程,它被终止掉,留下新的子进程继续运行。再次fork的目的是确保本守护进程将来即使打开了一个终端设备,也不会自动获得控制终端。当没有控制终端的一个会话头进程打开一个终端设备时(该终端不会是当前某个其他会话的控制终端),该终端自动成为这个会话头进程的控制终端。然而再次调用fork之后,我们确保新的子进程不再是一个会话头进程,从而不能自动获得一个控制终端。这里必须霍略SIGHUP信号,因为当会话头进程(即首次fork产生的子进程)终止时,其会话中的所有进程(即再次fork产生的子进程)都收到SIGHUP信号。 ### (4)将stdin,stdout和stderr重定向到/dev/null 因为之前关闭了所有的描述符,所以要打开这三个基本描述符并且重定向,让read返回0,write系统调用丢弃所写的数据(书上说如果调用了syslog函数,则不要调用类似printf之类的函数,因为会被简单的忽略掉)。因为如果继续关闭,则万一有新的进程打开一个描述符,却占用了0,1,2这三个描述符,则可能导致将错误的数据发送给客户端。 ## 3. inetd守护进程    旧的服务器只是等待客户请求的到达,如FTP,Telnet,TFTP等。这些进程都是在系统自举阶段从/etc/rc文件中启动,而且每个进程执行几乎相同的启动任务:创建一个套接字,把本服务器的众所周知端口捆绑到该套接字,等待一个连接或一个数据报,然后派生子进程。子进程为客户提供服务,父进程则继续等待下一个客户请求。这个模型存在两个问题: (1)所有这些守护进程含有几乎相同的启动代码,既表现在创建套接字上,也表现在演变成守护进程上(类似我们的daemon_init函数) (2)每个守护进程在进程表中占据一个表项,然而它们大部分时间处于睡眠状态。    而新版本的系统通过提供inetd守护进程(因特网超级服务器)来简化问题: (1)通过inetd处理普通守护进程的大部分启动细节来简化守护进程的编写。这么一来每个服务器不再有调用daemon_init函数的必要。 (2)单个进程就能为多个服务等待外来的客户请求,以此取代每个服务一个进程的做法。这么做减少了系统中的进程总数。 ### 1) inetd守护进程的工作流程 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b3084f7e.jpg) ### (0)对xinetd.conf文件的说明 | 字段 | 说明 | |-----|-----| | service_name | 必须在/etc/services文件中定义 | | socket_type | stream(对于tcp)或dgram(对于udp) | | protocol | 必须在/etc/protocols文件中定义:tcp或udp | | wait-falg | 对于TCP一半为nowait,对于UDP一般为wait | | login-name | 来自/etc/passwd的用户名,一般为root | | server-program | 调用exec指定的完整路径名 | | server-program-arguments | 调用exec指定的命令行参数 | 下面是xinetd.conf文件中的若干行: | ftp | stream | tcp | nowait | root | /usr/bin/ftpd | ftpd -l | |-----|-----|-----|-----|-----|-----|-----| | telnet | stream | tcp | nowait | root | /usr/bin/telnetd | telnetd | ### (1)socket()    在启动阶段,读入/etc/xinetd.conf文件并给该文件中指定的每个服务创建一个适当类型(字节流或数据报)的套接字。inetd能够处理的服务器的最大数目取决于inetd能够创建的描述符的最大数目。新创建的每个套接字都被加入到将由某个select调用使用的一个描述符集中。 ### (2)bind()    为每个套接字调用bind,指定捆绑相应服务器的众所周知端口和通配地址。这个TCP或UDP端口号通过调用getservbyname获得,作为函数参数的是相应服务器在配置文件中的service-name字段和protocol字段。 ### (3)listen()    对于每个TCP套接字,调用listen以接收外来的连接请求。对于数据报套接字则不执行本步骤 ### (4)select()等待可读条件    创建完毕所有套接字之后,调用select等待其中任何一个套接字变为可读。TCP监听套接字将在有一个新连接准备好可被接受时变为可读,UDP套接字将在有一个数据报到达时变为可读。inetd的不部分时间花在阻塞于select调用内部,等待某个套接字变为可读。 ### (5)accept()    当select返回指出某个套接字已可读之后,如果该套接字是一个TCP套接字,而且其服务器的wait-flag值为nowait,那就调用accept接受这个新连接。 ### (6)fork()    inetd守护进程调用fork派生进程,并由子进程处理服务请求。子进程关闭要处理的套接字描述符之外的所有描述符:对于TCP服务器来说,这个套接字是由accept返回的新的已连接套接字,对于UDP服务器来说,这个套接字是父进程最初创建的UDP套接字。子进程dup2三次,把这个待处理套接字的描述符复制到描述符0,1和2,然后关闭原套接字描述符(由accept返回的已连接的TCP套接字)。    子进程然后调用exec执行由相应的server-program字段指定的程序来具体处理请求,相应的server-program-arguments字段值则作为命令行参数传递给该程序。    如果第五步中的select返回的是一个字节流套接字,那么父进程必须关闭已连接套接字(就像标准并发服务器那样)。父进程再次调用select,等待下一个变为可读的套接字。(因为TCP设置的nowait,意味着inetd不必等待某个子进程终止就可以接收对于该子进程所提供之服务的另一个连接。如果对于某个子进程所提供之服务的另一个连接确实在该子进程终止之前到达:accept返回,那么父进程再次调用select:意味着要关闭已连接的套接字,继续执行步骤4,5,6)    给一个数据报服务指定wait标志导致父进程执行的步骤发生变化。这个标志要求inet必须在这个套接字再次称为slect调用的候选套接字之前等待当前服务该套接字的子进程终止。发生的变化有以下几点: [1]fork返回到父进程时,父进程保存子进程的进程ID。这么做使得父进程能够通过查看由waitpid返回的值确定这个子进程的终止时间 [2]父进程通过使用FD_CLR宏关闭这个套接字在select所用描述符集中对应的位,达成在将来的select调用中禁止这个套接字的目的。这点意味着子进程将接管该套接字,直到自身终止为止。 [3]当子进程终止时,父进程被通知一个SIGCHLD信号,而父进程的信号处理函数将取得这个子进程的进程ID。父进程通过打开相应的套接字在select所用描述符集中对应的位,使得该套接字重新成为select的候选套接字。 2)inetd守护进程的服务器程序 ~~~ #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <string.h> #include <signal.h> #include <fcntl.h> #include <unistd.h> #include <time.h> #include <netinet/in.h> #define MAXLINE 1024 int main(int argc, char **argv) { socklen_t len; struct sockaddr_in cliaddr; char buff[MAXLINE]; time_t ticks; openlog(argv[0], 0); len = sizeof(cliaddr); getpeername(0, (struct sockaddr *)&cliaddr, &len); inet_ntop(AF_INET, (struct sockaddr *)&cliaddr.sin_addr, buff, sizeof(buff)); printf("connect from %s\n", buff); ticks = time(NULL); snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks)); write(0, buff, strlen(buff)); close(0); exit(0); } ~~~ 在/etc/service中增加: ~~~ mydaytime 9999/tcp ~~~ 在/etc/xinetd.conf中增加: ~~~ mydaytime stream tcp nowait leichaojian /home/leichaojian/newdaytimetcpserv3 newdaytimetcpserv3 ~~~ 程序输出: ~~~ leichaojian@ThinkPad-T430i:~$ ./newdaytimetcpserv3 connect from 0.0.0.0 Fri Oct 3 14:27:31 2014 ~~~
';

UNP卷1:第十一章(名字与地址转换)

最后更新于:2022-04-01 14:49:05

### 1. gethostbyname函数 ~~~ #include <netdb.h> struct hostent *gethostbyname( const char *hostname ); 返回:若成功则为非空指针,若出错则为NULL且设置h_errno ~~~ 而hostent的结构如下: ~~~ struct hostent{ char *h_name; char *h_aliases; int h_addrtype; int h_length; char **h_addr_list; }; ~~~ 书上的例子如下:(实际上在ubuntu系统上,gethostbyname似乎会出错) ~~~ #include "myunp.h" int main( int argc, char **argv ) { char *ptr, **pptr; char str[ INET_ADDRSTRLEN ]; struct hostent *hptr; while ( --argc > 0 ){ ptr = *++argv; if ( ( hptr = gethostbyname( ptr ) ) == NULL ){ printf("gethostbyname error for host:%s:%s\n", ptr, hstrerror( h_errno ) ); continue; } printf("official hostname:%s\n", hptr->h_name ); for ( pptr = hptr->h_aliases; *pptr != NULL; pptr++ ) printf("\talias:%s\n", *pptr ); switch( hptr->h_addrtype ){ case AF_INET: pptr = hptr->h_addr_list; for ( ; *pptr != NULL; pptr++ ) printf("\taddress name:%s\n", *pptr ); // printf("\taddress:%s\n", inet_ntop( hptr->h_addrtype, *pptr, str, sizeof(str))); break; default: printf("unknown address type"); break; } } exit(0); } ~~~ 程序输出: ~~~ root@ThinkPad-T430i:/home/leichaojian# cc hostent.c -o hostent root@ThinkPad-T430i:/home/leichaojian# ./hostent www.baidu.com official hostname:www.a.shifen.com alias:www.baidu.com address name:�a!kwww.�a!lifen.com address name:�a!lifen.com ~~~    所以执行inet_ntop时候程序直接报异常。 ### 2. 测试inet_ntop和inet_pton函数 ~~~ #include "myunp.h" 2 3 int main( void ) 4 { 5 char *ptr = "127.0.0.1"; 6 char str[ 1024 ]; 7 struct sockaddr_in servaddr; 8 9 bzero( &servaddr, sizeof( servaddr ) ); 10 servaddr.sin_family = AF_INET; 11 servaddr.sin_port = htons( SERV_PORT ); 12 inet_pton( AF_INET, ptr, &servaddr.sin_addr); 13 14 inet_ntop( AF_INET, ( SA * )&servaddr.sin_addr, str, sizeof( str ) ); 15 16 printf("%s\n", str ); 17 return 0; 18 } ~~~ 程序输出: ~~~ leichaojian@ThinkPad-T430i:~$ ./test 127.0.0.1 ~~~ ###  3. getservbyname和getservbyport函数    一个根据给定名字查找相应服务,一个给定端口号和可选协议查找相应服务。 ~~~ #include <stdio.h> #include <netdb.h> int main( int argc, char **argv ) { struct servent *sptr; sptr = getservbyname( "domain", "udp" ); sptr = getservbyname( "ftp", "tcp" ); sptr = getservbyname( "ftp", "NULL" ); sptr = getservbyname( "ftp", "udp" ); sptr = getservbyport( htons( 53 ), "udp" ); sptr = getservbyport( htons( 21 ), "tcp" ); sptr = getservbyport( htons( 21 ), "NULL" ); sptr = getservbyport( htons( 21 ), "udp" ); return 0; } ~~~ 程序输出: ~~~ (gdb) break 8 Breakpoint 1 at 0x4005dc: file getservbynameport.c, line 8. (gdb) r Starting program: /home/leichaojian/getservbynameport Breakpoint 1, main (argc=1, argv=0x7fffffffde58) at getservbynameport.c:8 8 sptr = getservbyname( "domain", "udp" ); (gdb) n 9 sptr = getservbyname( "ftp", "tcp" ); (gdb) p *sptr $1 = {s_name = 0x602010 "domain", s_aliases = 0x602020, s_port = 13568, s_proto = 0x60201b "udp"} (gdb) n 10 sptr = getservbyname( "ftp", "NULL" ); (gdb) p *sptr $2 = {s_name = 0x602010 "ftp", s_aliases = 0x602020, s_port = 5376, s_proto = 0x602018 "tcp"} (gdb) n 11 sptr = getservbyname( "ftp", "udp" ); (gdb) p *sptr Cannot access memory at address 0x0 (gdb) n 13 sptr = getservbyport( htons( 53 ), "udp" ); (gdb) p *sptr Cannot access memory at address 0x0 (gdb) n 14 sptr = getservbyport( htons( 21 ), "tcp" ); (gdb) p *sptr $3 = {s_name = 0x603290 "domain", s_aliases = 0x6032a0, s_port = 13568, s_proto = 0x60329b "udp"} (gdb) n 15 sptr = getservbyport( htons( 21 ), "NULL" ); (gdb) p *sptr $4 = {s_name = 0x603290 "ftp", s_aliases = 0x6032a0, s_port = 5376, s_proto = 0x603298 "tcp"} (gdb) n 16 sptr = getservbyport( htons( 21 ), "udp" ); (gdb) p *sptr Cannot access memory at address 0x0 (gdb) n 18 return 0; ~~~ ### 4. getaddrinfo函数    此函数可用来代替gethostbyname和gethostbyaddr。 ~~~ #include <netdb.h> int getaddrinfo( const char *hostname, const char *service, const struct addrinfo *hints, struct addrinfo **result ); 返回:若成功则为0,若出错则为非0 ~~~ 参数1为主机名或地址串(点分十进制数串),service参数是一个服务名或十进制端口号数串,hints目前就直接置为NULL,而result则是我们需要的信息: ~~~ struct addrinfo{ int ai_flags; int ai_family; int ai_socktype; int ai_addrlen; socklen_t ai_addrlen; char *ai_canonname; struct sockaddr *ai_addr; struct addrinfo *ai_next; }; ~~~ 测试用例如下: ~~~ #include <stdio.h> #include <netdb.h> int main( int argc, char **argv ) { struct addrinfo hints, *res; struct sockaddr_in *addr; char str[ 1024 ]; getaddrinfo( "ThinkPad-T430i", "domain", NULL, &res ); for ( ;res->ai_next;res = res->ai_next ){ printf("ai_falgs:%d\n", res->ai_flags); printf("ai_family:%d\n", res->ai_family); printf("ai_socktype:%d\n", res->ai_socktype); printf("ai_addrlen:%d\n", res->ai_addrlen); printf("ai_canonname:%s\n", res->ai_canonname); addr = ( struct sockaddr_in * )res->ai_addr; printf("sin_family:%d\n", addr->sin_family); printf("sin_port:%d\n", ntohs( addr->sin_port ) ); inet_ntop( addr->sin_family, &addr->sin_addr, str, sizeof( str ) ); printf("sin_addr:%s\n", str); } return 0; } ~~~ 程序输出: ~~~ leichaojian@ThinkPad-T430i:~$ cc getaddrinfo.c leichaojian@ThinkPad-T430i:~$ ./a.out ai_falgs:40 ai_family:2 ai_socktype:1 ai_addrlen:16 ai_canonname:(null) sin_family:2 sin_port:53 sin_addr:127.0.1.1 ~~~ ### 5. 使用getaddrinfo来完成TCP时间获取服务器程序 ### 1)时间获取客户程序 ~~~ #include <stdio.h> #include <netdb.h> #include <sys/socket.h> #define MAXLINE 1024 int tcp_connect( const char *host, const char *serv ); int main( int argc, char **argv ) { int sockfd, n; char recvline[ MAXLINE + 1 ]; socklen_t len; struct sockaddr_in cliaddr; if ( argc != 3 ){ printf("argument should be 3\n"); exit(1); } sockfd = tcp_connect( argv[1], argv[2]); len = sizeof( cliaddr ); getpeername( sockfd, ( struct sockaddr * )&cliaddr, len ); inet_ntop( AF_INET, &cliaddr.sin_addr, recvline, sizeof( recvline ) ); printf("connect to %s\n", recvline); while ( ( n = read( sockfd, recvline, MAXLINE )) > 0 ){ recvline[ n ] = 0; fputs( recvline, stdout ); } exit( 0 ); } int tcp_connect( const char *host, const char *serv ) { int sockfd, n; struct addrinfo hints, *res, *ressave; struct sockaddr_in *cliaddr; bzero( &hints, sizeof( struct addrinfo ) ); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; if ( ( n = getaddrinfo( host, serv, &hints, &res ) ) != 0 ){ printf("tcp_connect error for %s,%s:%s", host, serv, gai_strerror(n)); exit(1); } ressave = res; do{ sockfd = socket( res->ai_family, res->ai_socktype, res->ai_protocol ); if ( sockfd < 0 ) continue; if ( connect( sockfd, res->ai_addr, res->ai_addrlen ) == 0 ) break; //用于调试 cliaddr = ( struct sockaddr_in * )res->ai_addr; close( sockfd ); } while ( ( res = res->ai_next ) != NULL ); if ( res == NULL ) printf("tcp_connect error for %s,%s\n", host, serv ); freeaddrinfo( ressave ); return ( sockfd ); } ~~~ 程序输出: ~~~ leichaojian@ThinkPad-T430i:~$ ./newdaytimetcpcli ThinkPad-T430i daytime connect to 0.0.0.0 30 SEP 2014 00:21:58 CST ~~~ 备注:这里要开启时间,具体操作如下 1) 安装 ~~~ leichaojian@ThinkPad-T430i:~$ sudo apt-get install xinetd ~~~ 2) 修改配置如下: ~~~ leichaojian@ThinkPad-T430i:~$ cd /etc/xinetd.d leichaojian@ThinkPad-T430i:/etc/xinetd.d$ vim daytime ~~~ 3) 具体修改: 将disable=yes改为disable=no(两处均要修改) 4)我不知道如何重新载入,所以直接重启了电脑。 ### 2)时间获取服务器程序 ~~~ #include <stdio.h> #include <netdb.h> #include <sys/socket.h> #include <time.h> #define MAXLINE 1024 int tcp_listen( const char *host, const char *serv, socklen_t *addrlenp ); int main( int argc, char **argv ) { int listenfd, connfd; socklen_t len; char buff[ MAXLINE ]; time_t ticks; struct sockaddr_in cliaddr; listenfd = tcp_listen( argv[ 1 ], argv[ 2 ], NULL ); for ( ; ; ){ len = sizeof( cliaddr ); connfd = accept( listenfd, ( struct sockaddr *)&cliaddr, &len ); inet_ntop( AF_INET, &cliaddr.sin_addr, buff, sizeof( buff ) ); printf("conneciton from %s\n", buff ); ticks = time( NULL ); snprintf( buff, sizeof( buff ), "%.24s\r\n", ctime(&ticks)); write( connfd, buff, strlen(buff)); close( connfd ); } } int tcp_listen( const char *host, const char *serv, socklen_t *addrlenp ) { int listenfd, n; const int on = 1; struct addrinfo hints, *res, *ressave; bzero( &hints, sizeof( struct addrinfo ) ); hints.ai_flags = AI_PASSIVE; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; if ( ( n = getaddrinfo( host, serv, &hints, &res ) ) != 0 ){ printf("tcp_listen error for %s,%s: %s\n", host, serv, gai_strerror(n)); } ressave = res; do{ listenfd = socket( res->ai_family, res->ai_socktype, res->ai_protocol ); if ( listenfd < 0 ) continue; setsockopt( listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); if ( bind( listenfd, res->ai_addr, res->ai_addrlen) == 0 ) break; close( listenfd ); } while(( res = res->ai_next) != NULL ); if ( res == NULL ){ printf("tcp_listen error for %s,%s", host, serv); } listen( listenfd, 5 ); if ( addrlenp ) *addrlenp = res->ai_addrlen; freeaddrinfo(ressave); return (listenfd ); } ~~~ 服务端运行: ~~~ leichaojian@ThinkPad-T430i:~$ ./newdaytimetcpserv ThinkPad-T430i 9877 conneciton from 127.0.0.1 ~~~ 客户端运行: ~~~ leichaojian@ThinkPad-T430i:~$ ./newdaytimetcpcli ThinkPad-T430i 9877 connect to 0.0.0.0 Tue Sep 30 18:14:25 2014 ~~~ ### 6. 使用getaddrinfo来完成UDP时间获取服务器程序 ### 1)时间获取客户端程序 ~~~ #include <stdio.h> #include <netdb.h> #include <sys/socket.h> #define MAXLINE 1024 int udp_client(const char *host, const char *serv, struct sockaddr **saptr, socklen_t *lenp ); int main(int argc, char **argv) { int sockfd, n; char recvline[ MAXLINE + 1 ]; socklen_t salen; struct sockaddr *sa; if (argc != 3){ printf("argument should be 3\n"); exit(1); } sockfd = udp_client(argv[1], argv[2], (void **)&sa, &salen); sendto(sockfd, "", 1, 0, sa, salen); n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL); recvline[n] = '\0'; fputs(recvline, stdout); exit(0); } int udp_client(const char *host, const char *serv, struct sockaddr **saptr, socklen_t *lenp ) { int sockfd, n; struct addrinfo hints, *res, *ressave; bzero(&hints, sizeof(struct addrinfo)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; if ((n = getaddrinfo(host, serv, &hints, &res)) != 0){ printf("udp_client error for %s,%s:%s\n", host, serv, gai_strerror(n)); exit(1); } ressave = res; do{ sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (sockfd >= 0) break; }while ((res = res->ai_next) != NULL); if (res == NULL){ printf("udp_client error for %s,%s\n", host, serv); exit(1); } *saptr = malloc(res->ai_addrlen); memcpy(*saptr, res->ai_addr, res->ai_addrlen); *lenp = res->ai_addrlen; freeaddrinfo(ressave); return (sockfd); } ~~~ 程序输出: ~~~ leichaojian@ThinkPad-T430i:~$ ./newdaytimeudpcli ThinkPad-T430i daytime 30 SEP 2014 18:37:24 CST ~~~ ### 2) 时间获取服务端程序 ~~~ #include <stdio.h> #include <netdb.h> #include <sys/socket.h> #include <time.h> #define MAXLINE 1024 int udp_server(const char *host, const char *serv, socklen_t *addrlenp); int main(int argc, char **argv) { int sockfd; ssize_t n; char buff[MAXLINE]; time_t ticks; socklen_t len; struct sockaddr_in cliaddr; sockfd = udp_server( argv[1], argv[2], NULL); for ( ; ; ){ len = sizeof(cliaddr); n = recvfrom(sockfd, buff, MAXLINE, 0, (struct sockaddr *)&cliaddr, &len); inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)); printf("datagram from %s\n", buff ); ticks = time(NULL); snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks)); sendto(sockfd, buff, strlen(buff), 0, (struct sockaddr *)&cliaddr, len); } } int udp_server(const char *host, const char *serv, socklen_t *addrlenp) { int sockfd, n; struct addrinfo hints, *res, *ressave; bzero(&hints, sizeof(struct addrinfo)); hints.ai_flags = AI_PASSIVE; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; if ((n = getaddrinfo(host, serv, &hints, &res)) != 0){ printf("udp_server error for %s,%s:%s", host, serv, gai_strerror(n)); exit(1); } ressave = res; do{ sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (sockfd < 0) continue; if (bind(sockfd, res->ai_addr, res->ai_addrlen) == 0) break; close(sockfd); }while ((res = res->ai_next) != NULL); if (res == NULL){ printf("udp_server error for %s,%s\n", host, serv); exit(1); } if (addrlenp) *addrlenp = res->ai_addrlen; freeaddrinfo(ressave); return (sockfd); } ~~~ 服务器运行: ~~~ leichaojian@ThinkPad-T430i:~$ ./newdaytimeudpserv ThinkPad-T430i 9877 datagram from 127.0.0.1 ~~~ 客户端运行: ~~~ leichaojian@ThinkPad-T430i:~$ ./newdaytimeudpcli ThinkPad-T430i 9877 Tue Sep 30 18:56:41 2014 ~~~
';

UNP卷1:第八章(基本UNP套接字编程)

最后更新于:2022-04-01 14:49:03

## 1. 简单的UDP回射程序 ### 1) 服务器udpsrv.c ~~~ #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> #include <sys/types.h> #define MAXLINE 1024 #define SA struct sockaddr void dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen); int main(int argc, char **argv) { int sockfd; struct sockaddr_in servaddr, cliaddr; sockfd = socket(AF_INET, SOCK_DGRAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(9877); bind(sockfd, (SA *)&servaddr, sizeof(servaddr)); dg_echo(sockfd, (SA *)&cliaddr, sizeof(cliaddr)); return 0; } void dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen) { int n; socklen_t len; char mesg[MAXLINE]; for ( ; ; ){ len = clilen; n = recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len); sendto(sockfd, mesg, n, 0, pcliaddr, len); } } ~~~ ### 2) 客户端udpcli.c ~~~ #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #define MAXLINE 1024 #define SA struct sockaddr void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen); int main(int argc, char **argv) { int sockfd; struct sockaddr_in servaddr; if (argc != 2){ printf("argument should be 2\n"); exit(-1); } bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(9877); inet_pton(AF_INET, argv[1], &servaddr.sin_addr); sockfd = socket(AF_INET, SOCK_DGRAM, 0); dg_cli(stdin, sockfd, (SA *)&servaddr, sizeof(servaddr)); exit(0); } void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen) { int n; char sendline[MAXLINE], recvline[MAXLINE + 1]; while (fgets(sendline, MAXLINE, fp) != NULL){ sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen); n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL); recvline[n] = 0; fputs(recvline, stdout); } } ~~~ 程序输入输出: 服务器: ~~~ leichaojian@ThinkPad-T430i:~$ ./udpsrv ~~~ 客户端: ~~~ leichaojian@ThinkPad-T430i:~$ ./udpcli 127.0.0.1 i love you i love you ^C ~~~ ### 3) 服务器进程未运行时候客户端的阻塞    如果服务器未启动而客户端发送一行文本,则客户永远阻塞于它的recvfrom调用,等待一个永不出现的服务器应答(所以下例中tcpdump只显示第一行hello world的信息,而其余的信息已经被阻塞了): 启动tcpdump: ~~~ root@ThinkPad-T430i:/home/leichaojian# tcpdump -i eth0 udp port 9877 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes 22:18:50.481559 IP 192.168.0.7.60341 > 218.30.64.194.9877: UDP, length 12 ~~~ 直接启动客户端: ~~~ leichaojian@ThinkPad-T430i:~$ ./udpcli 218.30.64.194 hello world i love you what ~~~    tcpdump只显示hello world发送的文本,而接着发送i love you和what则没有任何的反应,因为已经阻塞了。其中9877是服务器指定的总所周知的端口号,而60341为客户端临时端口。 ## 2. UDP的connect函数 ### 1) connect函数的起源    除非套接字已连接,否则异步错误是不会返回到UDP套接字的。我们可以给UDP套接字调用connect,然而这样做的结果却与TCP连接大相近庭:没有三次握手过程。内核只是检查是否存在立即可知的错误,记录对端的IP地址和端口号(取自传递给connect的套接字地址结构),然后立即返回到调用进程。    有了这个能力后,我们必须区分: (1) 未连接UDP套接字,新创建UDP套接字默认如此。 (2) 已连接UDP套接字,对UDP套接字调用connect的结果。    对于已连接UDP套接字,与默认的未连接UDP套接字相比,发生了三个变化: (1) 我们再也不能给输出操作指定目的IP地址和端口号。也就是说,我们不使用sendto而改用write或send。写到已连接UDP套接字上的任何内容都自动发送到由connect指定的协议地址(例如IP地址和端口号) (2) 我们不必使用recvfrom以获悉数据报的发送者,而改用read,recv或recvmsg。在一个已连接UDP套接字上,由内核为输入操作返回的数据报只有那些来自connect所指定协议地址的数据报。目的地为这个已连接UDP套接字的本地协议地址(例如IP地址和端口号),发源地却不是该套接字早先connect到的协议地址的数据报,不会投递到该套接字。这样就限制一个已连接UDP套接字能且仅能与一个对端交换数据报。 (3) 由已连接UDP套接字引发的异步错误会返回给它们所在的进程,而未连接UDP套接字不接收任何异步错误。 ### 2) 使用connect的客户端 ~~~ #include "myunp.h" void dg_cli( FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen ) { int n; char sendline[ MAXLINE ], recvline[ MAXLINE + 1 ]; Connect( sockfd, ( SA * )pservaddr, servlen ); while ( fgets( sendline, MAXLINE, fp ) != NULL ){ write( sockfd, sendline, strlen( sendline ) ); n = read( sockfd, recvline, MAXLINE ); if ( n < 0 ){ printf("read error\n"); return; } recvline[ n ] = 0; fputs( recvline, stdout ); } } int main( int argc, char **argv ) { int sockfd; struct sockaddr_in servaddr; bzero( &servaddr, sizeof( servaddr ) ); servaddr.sin_family = AF_INET; servaddr.sin_port = htons( 9877 ); inet_pton( AF_INET, argv[ 1 ], &servaddr.sin_addr ); sockfd = Socket( AF_INET, SOCK_DGRAM, 0 ); dg_cli( stdin, sockfd, ( SA * )&servaddr, sizeof( servaddr ) ); exit( 0 ); } ~~~    如果服务端不启动,则输出结果如下: 服务端: ~~~ root@ThinkPad-T430i:/home/leichaojian# tcpdump -i eth0 udp port 9877 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes 19:27:08.010090 IP 192.168.0.7.51328 > 218.30.64.194.9877: UDP, length 12 ~~~ 客户端分两种情况: 1)  ~~~ leichaojian@ThinkPad-T430i:~$ host leichaojian leichaojian.router has address 218.30.64.194 Host leichaojian.router not found: 5(REFUSED) Host leichaojian.router not found: 3(NXDOMAIN) leichaojian@ThinkPad-T430i:~$ ./dgcliconnect 218.30.64.194 hello world ^C ~~~ 2) 如果是以下的代码,则服务端无任何抓包行为: ~~~ leichaojian@ThinkPad-T430i:~$ ./dgcliconnect 127.0.0.1 hello world n is:-1 read error ~~~
';

UDP卷1:第六章(I/O复用:select和poll函数)

最后更新于:2022-04-01 14:49:00

## 0. 概述    在第五章的TCP客户同时处理两个输入:标准输入和TCP套接字。我们遇到的问题是在客户阻塞于fgets调用期间,服务器进程会被杀死。服务器TCP虽然正确的给客户TCP发送了一个FIN,但是既然客户进程正阻塞于从标准输入读入的过程,它将看不到这个EOF,直到从套接字读时为止(可能经过很长时间)。这样的进程需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或多个I/O条件就绪(也就是说输入已准备好被读取,或者描述符已能承接更多的输出),它就通知进程。这个能力称为I/O复用,是由select和poll这两个函数支持。    I/O复用使用于以下场合: 1) 当客户处理多个描述符(通常是交互式输入和网络套接字)时,必须使用I/O复用。 2) 一个客户同时处理多个套接字是可能的,不过比较少见。 3) 如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字,一般就要使用I/O复用。 4)如果一个服务器既要处理TCP,又要处理UDP,一般就要使用I/O复用。 5) 如果一个服务器要处理多个服务或者多个协议,一般就要使用I/O复用。 ## 1. I/O模型   一个输入操作通常包括两个不同的阶段: (1) 等待数据准备好 (2) 从内核向进程复制数据   对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。 ### 1) 阻塞式I/O模型 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b2fe2ff0.jpg)   进程调用recvfrom,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发生错误才返回。我们说进程在从调用recvfrom开始到它返回的整段时间内饰被阻塞的。recvfrom成功返回后,应用进程开始处理数据报。 ### 2) 非阻塞式I/O模型 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b3008f57.jpg)   进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入睡眠才能完成,不要把本进程投入睡眠,而是返回一个错误。    当一个应用进程像这样对一个非阻塞描述符循环调用recvfrom时,我们称之为轮询。应用进程持续轮询内核,以查看某个操作是否就绪。这么做往往耗费大量CPU时间。 ### 3) I/O复用模型 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b302424b.jpg)   我们阻塞于select调用(而非阻塞于recvfrom处),等待数据报套接字变为可读。当select返回套接字可读这一条件时,我们调用recvfrom把所读数据报复制到应用进程缓冲区。使用select的优势在于我们可以等待多个描述符就绪。 ### 4) 信号驱动式I/O模型 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b303e674.jpg)   让内核在描述符就绪时发送SIGIO信号通知我们。    无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。 ### 5) 异步I/O模型 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b305e567.jpg)   这些函数的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。 ## 2. select函数 ### 1) 基础知识    该函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。 ~~~ #include <sys/select.h> #include <sys/time.h> int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout ); 返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1 ~~~    作为一个例子,我们可以调用select,告知内核仅在下列情况发生时才返回: (1)集合{1,4,5}中的任何描述符准备好读 (2)集合{2,7}中的任何描述符准备好写 (3)集合{1,4}中的任何描述符有异常条件待处理 (4)已经经历了10.2秒    timeout告知内核等待所指定描述符中的任何一个就绪可花多长时间。其timeval结构用于指定这段时间的秒数和微秒数: ~~~ struct timeval{ long tv_sec; long tv_usec; }; ~~~    这个参数有以下三种可能: 1) 永远等待下去:仅在有一个描述符准备好I/O时才返回。为此,我们把这参数设置为空指针。 2) 等待一段固定时间:在有一个描述符准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。 3) 根本不等待:检查描述符后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0.    中间的三个参数readset,writeset和exceptset指定我们要让内核测试读,写和异常条件的描述符。    maxfdp1参数指定待测试的描述符个数,它的值是待测试的最大描述符加1.    关于fd_set结构体数据四个关键的宏: ~~~ void FD_ZERO( fd_set *fset ); void FD_SET( int fd, fd_set *fdset ); void FD_CLR( int fd, fd_set *fdset ); int FD_ISSET( int fd, fd_set *fset ); ~~~    假设我们要将描述符1(对应于stdout,标准输出),4,5(分别对应socket中服务器socket描述符和客户端的一个socket描述符)放入select函数中,当任何一个写就绪时候就返回,那么我们大概可以这样写: ~~~ fd_set rset; FD_ZERO( &rset ); FD_SET( 1, &rset ); FD_SET( 4, &rset ); FD_SET( 5, &rset ); select( maxfdp1, NULL, &rset, NULL,NULL); ~~~    描述符集的初始化非常重要,因为作为自动变量分配的一个描述符集如果没有初始化,那么可能发生不可预期的后果。 测试用力如下: ~~~ #include <stdio.h> #include <sys/socket.h> #include <sys/select.h> #include <time.h> int main(int argc, char **argv) { fd_set rset; FD_ZERO(&rset); FD_SET(1, &rset); FD_SET(4, &rset); FD_SET(5, &rset); return 0; } ~~~ 当我们调试程序,查看rset: ~~~ (gdb) p rset $3 = {__fds_bits = {50, 0 <repeats 15 times>}} (gdb) p rset.__fds_bits $4 = {50, 0 <repeats 15 times>} ~~~    其中,50=110010,即第1,4,5位均被置为1. ### 2) 描述符就绪条件 (1)满足下列四个条件中的任何一个时,一个套接字准备好读(即可从描述符中读取数据) a) 该套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的当前大小。对这样的套接字执行读操作不会阻塞并将返回一个大于0的值(也就是返回准备好读入的数据,即进程可以从缓冲区中读取数据) b) 该连接的读半部关闭(也就是接收了FIN的TCP连接)。对这样的套接字的读操作将不阻塞并返回0(因为这时候服务器执行close套接字需要一段时间,而这段时间内,客户端可继续从服务器读取数据,只是读取的是EOF而已) c) 该套接字是一个监听套接字且已完成的连接数不为0.(这样服务端才能执行accept函数,读取客户端发送过来的数据) d) 其上有一个套接字错误待处理。对这样的套接字的读操作将不阻塞并返回-1,同时把errno设置成确切的错误条件。 (2)满足下列四个条件中的任何一个时,一个套接字准备好写(即可向描述符中写入数据) a) 该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前大小,并且或者该套接字已连接,或者该套接字不需要连接。 b) 该连接的写半部关闭。对这样的套接字的写操作将产生SIGPIPE信号。(就是如果服务器不启动,而客户端启动向服务器发送数据,则服务端向客户端发送RST,并且向客户端写入数据(相当于客户端读取数据),则产生SIGPIPE信号,进程强行终止) c) 使用非阻塞式connect的套接字已建立连接,或者connect已经以失败告终。(只有成功connect,才能进行数据的写入) d) 其上有一个套接字错误待处理。    注意:当某个套接字上发生错误时,它将由select标记为即可读又可写。    接收低水位标记和发送低水位标记的目的在于:允许应用进程控制在select返回可读或可写条件之前有多少数据可读或有多大空间可用于写。举例来说,如果我们知道除非至少存在64个字节的数据,否则我们的应用进程没有任何有效工作可做,那么可以把接收低水位标记设置为64,以防少于64个字节的数据准备好读时select唤醒我们。 ### 3) 使用select的str_cli函数的实现 客户的套接字上的三个条件处理如下: (1)如果对端TCP发送数据,那么该套接字变为可读,并且read返回一个大于0的值(即读入数据的字节数) (2)如果对端TCP发送一个FIN(对端进程终止),那么该套接字变为可读,并且read返回0(EOF)。 (3)如果对端TCP发送一个RST(对端主机崩溃并重新启动),那么该套接字变为可读,并且read返回-1,而errno中含有确切的错误码。 ~~~ void str_cli( FILE *fp, int sockfd ) { int maxfdp1; fd_set rset; char sendline[ MAXLINE ], recvline[ MAXLINE ]; FD_ZERO(&rset); for( ; ; ){ FD_SET(fileno(fp), &rset); FD_SET(sockfd, *rset); maxfdp1 = max(fileno(fp), sockfd) + 1; select(maxfdp1, &rset, NULL, NULL, NULL); if ( FD_ISSET(sockfd,&rset)){ if ( Readline(sockfd, recvline, MAXLINE) == 0) err_quit("str_cli:server terminated prematurely"); Fputs(recvline, stdout); } if ( FD_ISSET(fileno(fp), &rset)){ if ( Fgets(sendline, MAXLINE, fp) == NULL) return; Writen(sockfd, sendline, strlen(sendline)); } } } ~~~ ### 4) 使用select版本的str_cli函数仍不正确,但问题出在哪里    如果我们批量输入的情况下,对标准输入中的EOF的处理:str_cli函数就此返回到main函数,而main函数随后终止。然而在批量方式下,标准输入中的EOF并不意味着我们同时也完成了从套接字的读入;可能仍有请求在去往服务器的路上,或者仍有应答在返回客户的路上。 (1)在fgets函数处返回单个输入行写给服务器,随后select再次被调用以等待新的工作,而不管stdio缓冲区中还有额外的输入行待消费。究其原因在于select不知道stdio使用了缓冲区---它只是从read系统调用的角度指出是否有数据可读,而不是从fgets之类调用的角度考虑。 (2)而在readline调用中,这回select不可见的数据不是隐藏在stdio缓冲区,而是隐藏在readline自己的缓冲区中。所以也可能导致程序终止时缓冲区中还有未读取的数据。 ## 3. shutdown函数,poll函数以及TCP回射服务器程序的修订版 ### 1) shutdown函数    终止网络连接的通常方法是调用close函数,不过close有两个限制,却可以使用shutdown来避免: (1)close把描述符的引用计数减1,仅在该计数变为0时才关闭套接字。使用shutdown可以不管引用计数就激发TCP的正常连接终止序列。 (2)close终止读和写两个方向的数据传送。这导致有些数据存于缓冲区内,并未被发送/接收成功。 ~~~ #include <sys/socket.h> int shutdown( int sockfd, int howto ); 返回:若成功则为0,若出错则为-1 ~~~ 该函数依赖于howto参数的值: SHUT_RD:关闭连接的读这一半----套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据都被丢弃。进程不能再对这样的套接字调用任何读函数。对一个TCP套接字这样调用shutdown函数后,由该套接字接收的来自对端的任何数据都被确认,然后悄然丢弃。 SHUT_WR:关闭连接的写这一半----对于TCP套接字,这称为半关闭。当前留在套接字发送缓冲区中的数据将被发送掉,后跟TCP的正常连接终止序列。 SHUT_RDWR:连接的读半部和写半部都关闭----这与调用shutdown两次等效:第一个调用指定SHUT_RD,第二次调用指定SHUT_WR. ### 2)str_cli函数的修订版 (1)我们使用read和write函数处理缓冲区而非文本,可以保证缓冲区的数据完全的读取。 (2)如果执行了err_quit函数,则说明服务器过早的终止。 (3)使用shutdown(sockfd,SHUT_WR)的作用是:终止写入,并且把缓冲区所有的数据全部发送出去 ~~~ void str_cli( FILE *fp, int sockfd ) { int maxfdp1, stdineof; fd_set rset; char buf[ MAXLINE ]; int n; stdineof = 0; FD_ZERO(&rset); for( ; ; ){ if ( stdineof == 0 ) FD_SET(fileno(fp), &rset); FD_SET(sockfd, *rset); maxfdp1 = max(fileno(fp), sockfd) + 1; select(maxfdp1, &rset, NULL, NULL, NULL); /*read和write是对缓冲区进行操作*/ if ( FD_ISSET(sockfd,&rset)){ if ( ( n = Read(sockfd,buf,MAXLINE)) == 0){ if ( stdineof == 1 ) return; else err_quit("str_cli:server terminated prematurely"); } Write(fileno(stdout),buf,n); } if ( FD_ISSET(fileno(fp), &rset)){ //说明数据已经从缓冲区中读取完毕,即全部数据都发送给进程 if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0){ stdineof = 1; Shutdown(sockfd,SHUT_WR); FD_CLR(fileno(fp),&rset); continue; } //因为执行了Shutdown(sockfd,SHUT_WR);说明所有存在缓冲区的数据,均被发送到了sockfd Writen(sockfd, sendline, strlen(sendline)); } } } ~~~ ### 3) TCP回射服务器程序(修订版) 服务端: ~~~ #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> #include <errno.h> #include <sys/select.h> #define MAXLINE 1024 #define SA struct sockaddr int main(int argc, char **argv) { int i, maxi, maxfd, listenfd, connfd, sockfd; int nready, client[FD_SETSIZE]; ssize_t n; fd_set rset, allset; char buf[MAXLINE]; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; listenfd = socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(9877); bind(listenfd, (SA *)&servaddr, sizeof(servaddr)); listen(listenfd, 5); maxfd = listenfd; maxi = -1; for (i = 0; i < FD_SETSIZE; i++) client[i] = -1; FD_ZERO(&allset); FD_SET(listenfd, &allset); for ( ; ; ){ rset = allset; nready = select(maxfd + 1, &rset, NULL, NULL, NULL); if (FD_ISSET(listenfd, &rset)){ clilen = sizeof(cliaddr); connfd = accept(listenfd, (SA *)&cliaddr, &clilen); for (i = 0; i < FD_SETSIZE; i++) if (client[i] < 0){ client[i] = connfd; break; } if (i == FD_SETSIZE){ printf("too many clients\n"); exit(-1); } FD_SET(connfd, &allset); if (connfd > maxfd) maxfd = connfd; if (i > maxi) maxi = i; if (--nready <= 0) continue; } for (i = 0; i <= maxi; i++){ if ((sockfd = client[i]) < 0) continue; if (FD_ISSET(sockfd, &rset)){ if ((n = read(sockfd, buf, MAXLINE)) == 0){ close(sockfd); FD_CLR(sockfd, &allset); client[i] = -1; } else write(sockfd, buf, n); if (--nready <= 0) break; } } } } ~~~ 客户端: ~~~ #include <stdio.h> #include <stdlib.h> #include <netinet/in.h> #include <fcntl.h> #define MAXLINE 1024 #define SA struct sockaddr void str_cli(FILE *fp, int sockfd); int main(int argc, char **argv) { int sockfd[5], n; struct sockaddr_in servaddr; struct sockaddr_in cliaddr; int i; for (i = 0; i < 5; i++){ sockfd[i] = socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(9877); inet_pton(AF_INET, argv[1], &servaddr.sin_addr); connect(sockfd[i], (SA *)&servaddr, sizeof(servaddr)); } str_cli(stdin, sockfd[0]); return 0; } void str_cli(FILE *fp, int sockfd) { char sendline[MAXLINE], recvline[MAXLINE]; int n; while (fgets(sendline, MAXLINE, fp) != NULL){ write(sockfd, sendline, strlen(sendline)); if (( n = read(sockfd, recvline, MAXLINE)) == 0){ printf("str_cli:server terminated prematurely\n"); return; } recvline[n] = '\0'; fputs(recvline, stdout); } } ~~~ 程序输出: 服务端: ~~~ leichaojian@ThinkPad-T430i:~$ ./srv ~~~ 客户端1: ~~~ leichaojian@ThinkPad-T430i:~$ ./cli 127.0.0.1 hello world hello world what what ^C ~~~ 客户端2: ~~~ leichaojian@ThinkPad-T430i:~$ ./cli 127.0.0.1 heihei heihei ^C ~~~
';

UNP卷1:第五章(TCP客户/服务器程序示例)

最后更新于:2022-04-01 14:48:58

## 1. 经典的回射程序 ### 1) 服务器程序srv.c ~~~ #include <stdio.h> #include <stdlib.h> #include <time.h> #include <sys/socket.h> #include <sys/types.h> #include <fcntl.h> #include <netinet/in.h> #include <errno.h> #define MAXLINE 1024 #define SA struct sockaddr void str_echo(int sockfd); int main(int argc, char **argv) { int listenfd, connfd; int buff[MAXLINE]; pid_t pid; struct sockaddr_in servaddr; struct sockaddr_in cliaddr; socklen_t cliLen; listenfd = socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(9877); bind(listenfd, (SA *)&servaddr, sizeof(servaddr)); listen(listenfd, 5); for ( ; ; ){ cliLen = sizeof(cliaddr); connfd = accept(listenfd, (SA *)&cliaddr, &cliLen); if ((pid = fork()) == 0){ close(listenfd); str_echo(connfd); _exit(0); } if (waitpid(pid, NULL, 0) != pid){ printf("waitpid error\n"); exit(1); } close(connfd); } return 0; } void str_echo(int sockfd) { ssize_t n; char buf[MAXLINE]; again: while ((n = read(sockfd, buf, MAXLINE)) > 0){ buf[n] = '\0'; write(sockfd, buf, n); } if (n < 0 && errno == EINTR) goto again; else if (n < 0) printf("str_echo:read error\n"); }维护子进程的信息,以便父进程在以后某个时候获取。这些信息包括子进程的进程ID,终止状 ~~~ ### 2) 客户端程序cli.c ~~~ #include <stdio.h> #include <stdlib.h> #include <netinet/in.h> #include <fcntl.h> #define MAXLINE 1024 #define SA struct sockaddr void str_cli(FILE *fp, int sockfd); int main(int argc, char **argv) { int sockfd, n; struct sockaddr_in servaddr; char buff[MAXLINE + 1]; struct sockaddr_in cliaddr; socklen_t cliLen; sockfd = socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(9877); inet_pton(AF_INET, argv[1], &servaddr.sin_addr); connect(sockfd, (SA *)&servaddr, sizeof(servaddr)); str_cli(stdin, sockfd); return 0; } void str_cli(FILE *fp, int sockfd) { char sendline[MAXLINE], recvline[MAXLINE]; while (fgets(sendline, MAXLINE, fp) != NULL){ write(sockfd, sendline, strlen(sendline)); if (read(sockfd, recvline, MAXLINE) == 0){ printf("str_cli:server terminated prematurely\n"); return; } fputs(recvline, stdout); } } ~~~ ### 3)程序运行 ### (1)服务器后台启动 ~~~ leichaojian@ThinkPad-T430i:~$ ./srv & [1] 3932 leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877 tcp 0 0 *:9877 *:* LISTEN ~~~ ### (2)启动客户端,并且键入一行文本 客户端: ~~~ leichaojian@ThinkPad-T430i:~$ ./cli 127.0.0.1 hello world hello world ~~~ 服务端: ~~~ leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877 tcp 0 0 *:9877 *:* LISTEN tcp 0 0 localhost:9877 localhost:43399 ESTABLISHED tcp 0 0 localhost:43399 localhost:9877 ESTABLISHED ~~~ ### (3)客户端终止 ~~~ leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877 tcp 0 0 *:9877 *:* LISTEN tcp 0 0 localhost:43399 localhost:9877 TIME_WAIT ~~~ ## 2. 处理信号 ### 1) POSIX信号处理    信号就是告知某个进程发生了某个事件的通知,有时也称为软件中断。信号通常是异步发生的,也就是说进程预先不知道信号的准确发生时刻。    信号可以: (1)由一个进程发给另一个进程。 (2)由内核发给某个进程。    每个信号都有一个与之关联的处置,也称为行为: (1)我们可以提供一个函数,只要有特定信号发生它就会被调用。这样的函数称为信号处理函数,这种行为称为捕获信号。有两个信号不能被捕获,它们是SIGKILL和SIGSTOP。信号处理函数由信号值这个单一的整数参数来调用,且没有返回值,其函数原型如下: ~~~ void handler( int signo ); ~~~ (2)我们可以把某个信号的处置设定为SIG_IGN来忽略它。SIGKILL和SIGSTOP这两个信号不能被忽略。 (3)我们可以把某个信号的处置设定为SIG_DFL来启用它的默认处置。 ### 2) 处理SIGCHLD信号    设置僵尸状态的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息包括子进程的进程ID,终止状态以及资源利用信息。如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程的父进程ID将被重置为1(init进程)。继承这些子进程的init进程将清理它们。    而僵尸进程出现时间是在子进程终止后,但是父进程尚未读取这些数据之前。所有解决之道就是保证父进程处理这些数据,我们可以通过wait或者waitpid函数来达到这个要求。    由于子进程的终止必然会产生信号SIGCHLD信号,所以重写TCP服务器程序最终版本: ### (1)服务器程序srv.c ~~~ #include <stdio.h> #include <netinet/in.h> #include <stdlib.h> #include <sys/socket.h> #include <signal.h> #include <errno.h> #define MAXLINE 1024 #define SA struct sockaddr void sig_chld(int signo); typedef void Sigfunc(int); Sigfunc *Signal(int signo, Sigfunc *func); void str_echo(int sockfd); int main(int argc, char **argv) { int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in servaddr, cliaddr; listenfd = socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(9877); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); bind(listenfd, (SA *)&servaddr, sizeof(servaddr)); listen(listenfd, 5); Signal(SIGCHLD, sig_chld); for ( ; ; ){ clilen = sizeof(cliaddr); if ((connfd = accept(listenfd, (SA *)&cliaddr, &clilen)) < 0){ if (errno == EINTR) continue; else{ printf("accept error\n"); exit(-1); } } if ((childpid = fork()) == 0){ close(listenfd); str_echo(connfd); exit(0); } close(connfd); } return 0; } void sig_chld(int signo) { pid_t pid; int stat; while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) printf("child %d terminated\n", pid); return; } Sigfunc *Signal(int signo, Sigfunc *func) { struct sigaction act, oact; act.sa_handler = func; sigemptyset(&act.sa_mask); act.sa_flags = 0; if (signo == SIGALRM){ #ifdef SA_INTERRUPT act.sa_flags |= SA_INTERRUPT; #endif } else { #ifdef SA_RESTART act.sa_flags |= SA_RESTART; #endif } if (sigaction(signo, &act, &oact) < 0) return (SIG_ERR); return (oact.sa_handler); } void str_echo(int sockfd) { char buff[MAXLINE]; int n; for ( ; ; ){ if ((n = read(sockfd, buff, MAXLINE)) > 0){ buff[n] = '\0'; write(sockfd, buff, n); } else if (n < 0 && errno == EINTR) continue; else if (n < 0){ printf("str_echo:read error\n"); return; } else if (n == 0){ break; } } } ~~~ ### (2)客户端测试程序cli.c ~~~ #include <stdio.h> #include <stdlib.h> #include <netinet/in.h> #include <fcntl.h> #define MAXLINE 1024 #define SA struct sockaddr void str_cli(FILE *fp, int sockfd); int main(int argc, char **argv) { int sockfd[5], n; struct sockaddr_in servaddr; struct sockaddr_in cliaddr; int i; for (i = 0; i < 5; i++){ sockfd[i] = socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(9877); inet_pton(AF_INET, argv[1], &servaddr.sin_addr); connect(sockfd[i], (SA *)&servaddr, sizeof(servaddr)); } str_cli(stdin, sockfd[0]); return 0; } void str_cli(FILE *fp, int sockfd) { char sendline[MAXLINE], recvline[MAXLINE]; while (fgets(sendline, MAXLINE, fp) != NULL){ write(sockfd, sendline, strlen(sendline)); if (read(sockfd, recvline, MAXLINE) == 0){ printf("str_cli:server terminated prematurely\n"); return; } fputs(recvline, stdout); } } ~~~ 程序输出如下: 客户端: ~~~ leichaojian@ThinkPad-T430i:~$ ./cli 127.0.0.1 hello world hello world ^C ~~~ 服务端: ~~~ leichaojian@ThinkPad-T430i:~$ ./srv child 9831 terminated child 9835 terminated child 9832 terminated child 9833 terminated child 9834 terminated ^C ~~~ ### 3) 测试僵尸进程的产生 test.c: ~~~ #include <stdio.h> #include <signal.h> #include <sys/wait.h> int main( void ) { pid_t pid; if ( ( pid = fork() ) == 0 ){ printf("child:%d\n", getpid()); exit(0); } sleep( 20 ); if ( pid > 0 ){ printf("parent:%d\n", getpid() ); } return 0; } ~~~ 程序运行: ~~~ leichaojian@ThinkPad-T430i:~$ ./a.out child:14447 parent:14446 ~~~ 在显示child:14447而尚未显示parent:14446(即20秒的睡眠时间),我们执行如下命令: ~~~ leichaojian@ThinkPad-T430i:~$ ps -eo state,pid,cmd | grep '^Z' Z 14447 [a.out] <defunct> ~~~    发现子进程14447果真称为僵尸进程。但是过了20秒后,再次执行时候,则没有任何数据,说明僵尸进程已经被父进程杀死了(就是父进程读取了子进程的数据) ### 4) 服务器进程终止 具体操作如下: ### 1)运行服务器程序,运行客户端程序: 服务端: ~~~ leichaojian@ThinkPad-T430i:~$ ./tcpserv ~~~ 客户端: ~~~ leichaojian@ThinkPad-T430i:~$ ./tcpcli 127.0.0.1 hello hello ~~~ 监视端: ~~~ leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877 tcp 0 0 *:9877 *:* LISTEN tcp 0 0 localhost:37935 localhost:9877 ESTABLISHED tcp 0 0 localhost:9877 localhost:37935 ESTABLISHED ~~~ ### 2) 终止服务器程序(先终止服务器程序,然后执行监视端,再执行客户端,再执行监视端) 监视端: ~~~ leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877 tcp 0 0 localhost:9877 localhost:37953 FIN_WAIT2 tcp 1 0 localhost:37953 localhost:9877 CLOSE_WAIT ~~~ 客户端: ~~~ leichaojian@ThinkPad-T430i:~$ ./tcpcli 127.0.0.1 hello hello world str_cli error ~~~ 监视端:(无任何输出,说明客户端进程已经终止,这里终止是产生了信号,强行终止) 来自UNP上的解释是:当一个进程向某个已收到RST的套接字执行写操作时,内核向该进程发送一个SIGPIPE信号。该信号的默认行为是终止进程,因此进程必须捕获它以免不情愿的被终止。 ~~~ leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877 ~~~ ### 3) 问题出在哪里?    当服务端的FIN到达套接字时,客户正阻塞与fgets调用上。客户实际上在应对两个描述符--套接字和用户输入,它不能单纯阻塞在这两个源中的某个特定源的输入上(正如目前编写的str_cli函数所为),而是应该阻塞在其中任何一个源的输入上,这正是select和poll这两个函数的目的之一。
';

UNP卷1:第四章(基本TCP套接字编程)

最后更新于:2022-04-01 14:48:56

## 0.TCP连接的建立和终止 ### 1) 三次握手    建立一个TCP连接时会发生下述情形: (1)服务器必须准备好接受外来的连接。这通常通过调用socket,bind和listen这三个函数来完成,我们称之为被动打开。 (2)客户通过调用connect发起主动打开。这导致客户TCP发送一个SYN(同步)分节,它告诉服务器客户将在(待建立的)连接中发送的数据的初始序列号。通常SYN分节不携带数据,其所在IP数据报只含有一个IP首部,一个TCP首部及可能有的TCP选项。 (2-1)TCP选项之MSS选项: 发送SYN的TCP一端使用本选项通告对端它的最大分节大小(maximum segment size)即MSS,也就是它在本连接的每个TCP分节中愿意接受的最大数据量。发送端TCP使用接收端的MSS值作为所发送分节的最大大小。 (3)服务器必须确认(ACK)客户的SYN,同时自己也得发送一个SYN分节,它含有服务器将在同一连接中发送的数据的初始序列号。服务器在单个分节中发送SYN和对客户SYN的ACK。 (4)客户必须确认服务器的SYN。    这种交换至少需要3个分组,因此称之为TCP的三路握手。(这里SYN为1字节,所以ACK时候只要在K上简单加1即可) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b2f0b396.jpg) ### 2) TCP连接终止    TCP终止一个连接则需4个分节: (1)某个应用进程首先调用close,我们称该端执行主动关闭。该端的TCP于是发送一个FIN分节,表示数据发送完毕。 (2)接收到这个FIN的对端执行被动关闭。这个FIN由TCP确认。它的接收也作为一个文件结束符(EOF)传递给接收端应用进程(放在已排队等候该应用进程接收的任何其他数据之后),因为FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。 (3)一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。 (4)接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。 备注:从执行被动关闭到执行主动关闭(步骤2和步骤3之间)一端流动数据是可能的,这称为半关闭,毕竟当时接收端的套接字并未close掉。(FIN和SYN一样为1字节,所以ACK也是简单的N+1) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b2f2d781.jpg) ### 3) TCP状态转换图 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b2f55bb3.jpg) ### 4) 观察分组 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b2f81d9a.jpg) ## 1. socket函数    为了执行网络I/O,一个进程必须做的第一件事就是调用socket函数,指定期望的通信协议类型。 ~~~ #include <sys/socket.h> int socket( int family, int type, int protocol ); 返回:若成功则为非负描述符,若出错则为-1 ~~~ ### 1) socket函数的family常值 | family | 说明 | |-----|-----| | AF_INET | IPv4协议 | | AF_INET6 | IPv6协议 | | AF_LOCAL | unix域协议 | | AF_ROUTE | 路由套接字 | | AF_KEY | 密钥套接字 | ### 2) socket函数的type常值 | type | 说明 | |-----|-----| | SOCK_STREAM | 字节流套接字 | | SOCK_DGRAM | 数据报套接字 | | SOCK_SEQPACKET | 有序分组套接字 | | SOCK_RAW | 原始套接字 | ### 3) socket函数AF_INET或AF_INET6的protocol常值 | protocol | 说明 | |-----|-----| | IPPROTO_TCP | TCP传输协议 | | IPPROTO_UDP | UDP传输协议 | | IPPROTO_SCTP | SCTP传输协议 |    socket函数在成功时返回一个小的非负整数值,它与文件描述符类似,我们把它称为套接字描述符,简称sockfd.    对于unix一切皆文件,则套接字描述符为网络通信中的文件描述符。程序可以通过套接字描述符进行通信。 ## 2. connect函数    TCP客户用connect函数来建立与TCP服务器的连接 ~~~ #include <sys/socket.h> int connect( int sockfd, const struct sockaddr *servaddr, socklen_t addrlen ); 返回:若成功则为0,若出错则为-1 ~~~    sockfd是由socket函数返回的套接字描述符,第二个,第三个参数分别是一个指向套接字地址结构的指针和该结构的大小。    客户在调用函数connect前不必非得调用bind函数,因为如果需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口。    如果是TCP套接字,调用connect函数将激发TCP的三路握手过程,而且仅在连接建立成功或出错时才返回,其中错误返回可能有以下几种情况: 1)若TCP客户没有收到SYN分节的响应,则返回ETIMEDOUT错误(超时) 2) 若对客户的SYN的响应是RST(表示复位),则表明该服务器主机在我们指定的端口上没有进程在等待与之连接(例如服务器进程也许没在运行,毕竟端口用于标识一个进程)。这是一种硬件错误(hard error),客户一接收到RST就马上返回ECONNREFUSED错误。    RST是TCP在发生错误时发送的一种TCP分节。产生RST的三个条件是:目的地为某端口的SYN到达,然而该端口上没有正在监听的服务器;TCP想取消一个已有连接;TCP接收到一个根本不存在的连接上的分节。 3)若客户发出的SYN在中间的某个路由器上引发了一个“destination unreadchable”ICMP错误,则认为是一种软件错误(soft error)。客户主机内核保存该消息,并继续发送SYN。若超时,则将ICMP错误作为EHOSTUNREACH或ENETUNREACH错误返回给进程。    若connect失败则该套接字不再可用,必须关闭,我们不能对这样的套接字再次调用connect函数。当循环调用函数connect为给定主机尝试各个IP地址直到有一个成功时,在每次connect失败后,都必须close当前的套接字描述符并重新调用socket。 ## 3. bind函数    bind函数把一个本地协议地址赋予一个套接字。 ~~~ #include <sys/socket.h> int bind( int sockfd, const struct sockaddr *myaddr, socklen_t addrlen ); 返回:若成功则为0,若出错则为-1 ~~~    第二个参数是一个指向特定与协议的地址结构的指针,第三个参数是该地址结构的长度。 1) 服务器在启动时捆绑它们的总所周知端口(端口用于标识一个进程,如果端口为0,则由内核选择端口,而且必须使用getsockname来返回协议地址来得到内核所选择的这个端口号) 2) 进程可以把一个特定的IP地址捆绑到它的套接字上(一般都是通配地址,用常量值INADDR_ANY来指定,如htonl( INADDR_ANY)) ## 4. listen函数    listen函数仅由TCP服务器调用,它做两件事情: 1) 当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用connect发起连接的客户套接字。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。 2) 第二个参数规定了内核应该为相应套接字排队的最大连接个数。 ~~~ #include <sys/socket.h> int listen( int sockfd, int backlog ); 返回:若成功则为0,若出错则为-1 ~~~ ### (1) 理解backlog 内核为任何一个给定的监听套接字维护两个队列: 未完成连接队列:每个这个的SYN分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于SYN_RCVD状态。 已完成连接队列:每个已完成TCP三路握手过程的客户对应其中一项。这些套接字处于ESTABLISHED状态。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b2fa8bed.jpg)    每当在未完成连接队列中创建一项时,来自监听套接字的参数就复制到即将建立的连接中。连接的创建机制是完全自动的,无需服务器进程握手: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-20_57678b2fc3200.jpg) ## 5. accept函数    accept函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠(假定套接字为默认的阻塞方式) ~~~ #include <sys/socket.h> int accept( int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen ); 返回:若成功则为非负描述符,若出错则为-1 ~~~    参数cliaddr和addrlen用来返回已连接的对端进程(客户)的协议地址。addrlen是值-结果参数:调用前,我们将由*addrlen所引用的整数值置为由cliaddr所指的套接字地址结构的长度,返回时,该整数值即为由内核存放在该套接字地址结构内的确切字节数。    我们在srv.c中增加以下代码,就可以看到客户端的IP和端口了: ~~~ socklen_t len; struct sockaddr_in servaddr, cliaddr; len = sizeof(cliaddr); connfd = accept( listenfd, ( SA * )&cliaddr, &len ); inet_ntop( AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)); printf("connection from %s,port %d\n", buff , ntohs(cliaddr.sin_port)); ~~~ 而服务端则会输出: ~~~ leichaojian@ThinkPad-T430i:~$ ./daytimetcpsrv1 connection from 127.0.0.1,port 57452 ~~~ ## 6. close函数    close函数也用来关闭套接字,并终止TCP连接。 ~~~ #include <unistd.h> int close( int sockfd ); ~~~ ## 7. getsockname和getpeername函数    这两个函数或者返回与某个套接字关联的本地协议地址(getsockname),或者返回与某个套接字关联的外地协议地址(getpeername) ### 1)getsockname的测试函数如下: 服务端: ~~~ #include <stdio.h> #include <stdlib.h> #include <time.h> #include <sys/socket.h> #include <sys/types.h> #include <fcntl.h> #include <netinet/in.h> #define MAXLINE 1024 #define SA struct sockaddr int main(int argc, char **argv) { int listenfd, connfd; int buff[MAXLINE]; pid_t pid; time_t ticks; struct sockaddr_in servaddr; struct sockaddr_in cliaddr; socklen_t cliLen; listenfd = socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(9877); bind(listenfd, (SA *)&servaddr, sizeof(servaddr)); listen(listenfd, 5); for ( ; ; ){ cliLen = sizeof(cliaddr); connfd = accept(listenfd, (SA *)&cliaddr, &cliLen); if ((pid = fork()) == 0){ close(listenfd); ticks = time(NULL); snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks)); write(connfd, buff, strlen(buff)); _exit(0); } if (waitpid(pid, NULL, 0) != pid){ printf("waitpid error\n"); exit(1); } close(connfd); } return 0; } ~~~ 客户端: ~~~ #include <stdio.h> #include <stdlib.h> #include <netinet/in.h> #include <fcntl.h> #define MAXLINE 1024 #define SA struct sockaddr int main(int argc, char **argv) { int sockfd, n; struct sockaddr_in servaddr; char buff[MAXLINE + 1]; struct sockaddr_in cliaddr; socklen_t cliLen; sockfd = socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(9877); inet_pton(AF_INET, argv[1], &servaddr.sin_addr); connect(sockfd, (SA *)&servaddr, sizeof(servaddr)); cliLen = sizeof(cliaddr); getsockname(sockfd, (SA *)&cliaddr, &cliLen); while ((n = read(sockfd, buff, MAXLINE)) > 0){ buff[n] = '\0'; fputs(buff, stdout); } return 0; } ~~~    当我们分别用gdb调试的时候,发现服务端和客户端的cliaddr的内容是一致的: ~~~ (gdb) p cliaddr $2 = {sin_family = 2, sin_port = 49635, sin_addr = {s_addr = 16777343}, sin_zero = "\000\000\000\000\000\000\000"} ~~~
';

UNP卷1:第三章(套接字编程简介)

最后更新于:2022-04-01 14:48:53

### 1. 套接字结构 ### 1) IPv4套接字地址结构 ~~~ truct in_addr{ in_addr_t s_addr; }; struct sockaddr_in{ uint8_t sin_len; sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; char sin_zero[8]; }; ~~~ 我们通常只使用三个字段:sin_family,sin_addr和sin_port.如下图所示: ~~~ bzero( &servaddr, sizeof( servaddr ) ); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(7777); ~~~ 而一个实例如下: ~~~ #include <stdio.h> #include <sys/stat.h> #include <netinet/in.h> int main( void ) { printf("%d\n", htonl(INADDR_ANY)); printf("%d\n", htonl(16)); printf("%d\n", htons(13)); return 0; } ~~~ 程序输出: ~~~ leichaojian@ThinkPad-T430i:~$ ./a.out 0 268435456 3328 ~~~ ### 2)通用套接字结构    当作为一个参数传递进任何套接字函数时,套接字地址结构总是以引用形式来传递。然而以这样的指针作为参数之一的任何套接字函数必须处理来自所支持的任何协议族的套接字地址结构。 ~~~ struct sockaddr{ uint8_t sa_len; sa_family_t sa_family; /*address family:AF_xxx value*/ char sa_data[14]; /*protocol-specific address*/ }; ~~~    于是套接字函数被定义为以指向某个通用套接字地址结构的一个指针作为其参数之一,类似bind函数: ~~~ bind(int, struct sockaddr *, socklen_t); ~~~    我们调用bind的时候,必须进行强制类型转换: ~~~ struct sockaddr_in serv; ....... bind(sockfd, (struct sockaddr *)&serv, sizeof(serv)); ~~~ ### 3)新的通用地址结构 ~~~ struct sockaddr_storage{ uint8_t sa_len; sa_family_t sa_family; }; ~~~    sockaddr_storage类型提供的通用套接字地址结构相比sockaddr存在以下两点差别: (1)如果系统支持的任何套接字地址结构有对齐需求,那么sockaddr_storage能够满足最苛刻的对齐要求。 (2)sockaddr_storage足够大,能够容纳系统支持的任何套接字地址结构。 备注:sockaddr_storage的其他字段对用户来说是透明的。 ### 2) 值-结果参数    当往一个套接字函数传递一个套接字地址结构时,该结构总是以引用形式来传递,也就是说传递的是指向该结构的一个指针。该结构的长度也作为一个参数来传递,不过其传递方式取决于该结构的传递方向:是从进程到内核,还是从内核到进程。    (1)从进程到内核传递套接字地址结构的函数有3个:bind,connect和sendto。这些函数的一个参数是指向某个套接字地址结构的指针,另一个参数是该结构的整数大小,例如: ~~~ struct sockaddr_in serv; connect( sockfd, ( SA * )&serv, sizeof( serv ) ); ~~~    既然指针和指针所指内容的大小都传递给了内核,于是内核知道到底需从进程复制多少数据进来。    (2)从内核到进程传递套接字地址结构的函数有4个:accept,recvfrom,getsockname和getpeername。 ~~~ struct sockaddr_un cli; socklen_t len; len = sizeof( cli ); getpeername( unixfd, ( SA * )&cli, &len ); /*len may have changed*/ ~~~    这里len既作为值传递进去,又作为结果返回回来。 ### 3) 字节排序函数 ### (1)大端模式---小端模式 ~~~ #include <stdio.h> int main( int argc, char **argv ) { union{ short s; char c[ sizeof( short ) ]; } un; un.s = 0x0102; if ( sizeof( short ) == 2 ){ if ( un.c[ 0 ] == 1 && un.c[ 1 ] == 2 ) printf("big-endian\n"); if ( un.c[ 0 ] == 2 && un.c[ 1 ] == 1 ) printf("little-endian\n"); else printf("unknown\n"); } else printf("sizeof(short)=%d\n", sizeof( short ) ); exit( 0 ); } ~~~    程序输出: ~~~ leichaojian@ThinkPad-T430i:~$ ./a.out little-endian ~~~ ### (2) 主机字节序和网络字节序之间相互转换的四个函数 ~~~ #include <netinet/in.h> uint16_t htons( uint16_t host16bitvalue ); uint32_t htonl( uint32_t host32bitvalue ); ------------均返回:网络字节序的值 uint16_t ntohs( uint16_t net16bitvalue ); uint32_t ntohl( uint32_t net32bitvalue ); ------------均返回:主机字节序的值 ~~~    在这些名字中,h代表host,n代表network,s代表short,l代表long。如今我们应该把s视为16位的值(如TCP或UDP端口号),把l视为一个32位的值(例如IPv4地址) 测试用例如下: ~~~ #include <stdio.h> #include <netinet/in.h> #include <sys/socket.h> int main(int argc, char **argv) { uint32_t ipAddr; uint16_t portAddr; struct sockaddr_in servaddr; char buff[1024]; bzero(&servaddr, sizeof(servaddr)); inet_pton(AF_INET, argv[1], &servaddr.sin_addr); ipAddr = ntohl(servaddr.sin_addr.s_addr); printf("ipAddr is:%d\n", ipAddr); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_addr.s_addr = htonl(ipAddr); inet_ntop(AF_INET, &servaddr.sin_addr, buff, sizeof(buff)); printf("the ip addr is:%s\n", buff); printf("------------\n"); portAddr = htons(9877); printf("the port 9877 netword port is:%d\n", portAddr); printf("the port is:%d\n", ntohs(portAddr)); return 0; } ~~~ 程序输出: ~~~ leichaojian@ThinkPad-T430i:~$ ./test 127.0.0.1 ipAddr is:2130706433 the ip addr is:127.0.0.1 ------------ the port 9877 netword port is:38182 the port is:9877 ~~~ ### 4) 相关的重要函数 ### (1)inet_aton和inet_ntoa函数 ~~~ #include <arpa/inet.h> int inet_aton( const char *strptr, struct in_addr *addrptr ); 返回:若字符串有效则为1,否则为0 char *inet_ntoa( struct in_addr inaddr ); 返回:指向一个点分十进制数串的指针 ~~~ 测试程序如下: ~~~ 1 #include <stdio.h> 2 #include <arpa/inet.h> 3 4 int main( int argc, char **argv ) 5 { 6 struct in_addr addr; 7 char *pAddr; 8 inet_aton( argv[ 1 ], &addr ); 9 printf( "%d\n", addr ); 10 pAddr = inet_ntoa( addr ); 11 printf("%s\n", pAddr ); 12 13 return 0; 14 } ~~~ 程序输出: ~~~ leichaojian@ThinkPad-T430i:~$ ./a.out 127.0.0.1 16777343 127.0.0.1 ~~~ ### (2)inet_pton和inet_ntop函数 ~~~ #include<arpa/inet.h> int inet_pton( int family, const char *strptr, void *addrptr ); const char *inet_ntop( int family, const void *addrptr, char *strptr, size_t len ); ~~~    这里p和n分别代表“表达”(presentation)和“数值”(numeric).即将点分十进制IP地址转换为套接字结构中的二进制值,或者逆向转换: ~~~ if ( inet_pton( AF_INET, argv[ 1 ], &servaddr.sin_addr ) <= 0 ){ printf("inet_pton error for %s", argv[1]); exit(1); } ~~~ ### (3)readn,writen(作者自己编写的)    字节流套接字上调用read或write输入或输出的字节数可能比请求的数量少,然而这不是出错的状态。这个现象的原因在于内核中用于套接字的缓冲区可能已达到极限,此时所需的是调用者再次调用read或write函数,以输入或输出剩余的字节。 ~~~ ssize_t readn(int fd, void *vptr, size_t n) { size_t nleft; ssize_t nread; char *ptr; ptr = vptr; nleft = n; while (nleft > 0){ if ((nread = read(fd, ptr, nleft)) < 0){ if (errno == EINTR) nread = 0; else return (-1); } else if (nread == 0) break; nleft --= nread; ptr += nread; } return (n - nleft); } ssize_t writen(int fd, const void *vptr, size_t n) { size_t nleft; ssize_t nwritten; const char *ptr; ptr = vptr; nleft = n; while (nleft > 0){ if ((nwritten = write(fd, ptr, nleft)) <= 0){ if (nwritten < 0 && errno == EINTR) nwritten = 0; else return (-1); } nleft -= nwritten; ptr += nwritten; } return (n); } ~~~
';

UNP卷1:第一章(简介)

最后更新于:2022-04-01 14:48:51

1. 时间获取程序 服务端:srv.c ~~~ #include <time.h> #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdlib.h> #include <fcntl.h> #define SA struct sockaddr #define MAXLINE 1024 int main( void ) { int listenfd, connfd; struct sockaddr_in servaddr; char buff[ MAXLINE ]; time_t ticks; pid_t pid; listenfd = socket( AF_INET, SOCK_STREAM, 0 ); bzero( &servaddr, sizeof( servaddr ) ); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(7777); bind(listenfd, ( SA * )&servaddr, sizeof(servaddr)); listen( listenfd, 5 ); for( ; ; ){ connfd = accept( listenfd, ( SA * )NULL, NULL); pid = fork(); if ( 0 == pid ){ ticks = time( NULL ); snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks)); write( connfd, buff, strlen(buff)); exit(1); } if ( waitpid(pid, NULL,0 ) != pid ){ printf("waitpid error"); exit(1); } close( connfd ); } return 0; } ~~~ 客户端:cli.c ~~~ #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdlib.h> #define SA struct sockaddr #define MAXLINE 1024 int main( int argc, char **argv ) { int sockfd, n; char recvline[ MAXLINE + 1 ]; struct sockaddr_in servaddr; if ( argc != 2 ){ printf("usage:a.out <IPaddress>"); exit(1); } if ( ( sockfd = socket( AF_INET, SOCK_STREAM, 0 ) ) < 0 ){ printf("socket error\n"); exit(1); } bzero( &servaddr, sizeof( servaddr ) ); servaddr.sin_family = AF_INET; servaddr.sin_port = htons( 7777 ); if ( inet_pton( AF_INET, argv[ 1 ], &servaddr.sin_addr ) <= 0 ){ printf("inet_pton error for %s", argv[1]); exit(1); } if ( connect( sockfd, ( SA * )&servaddr, sizeof( servaddr ) ) < 0 ){ printf("connect error\n"); exit(1); } while ( ( n = read( sockfd, recvline, MAXLINE )) > 0){ recvline[n] = 0; if (fputs(recvline, stdout) == EOF){ printf("fputs error\n"); exit(1); } } if (n < 0){ printf("read error\n"); exit(1); } exit(0); } ~~~ 运行结果如下: 服务端: ~~~ root@ThinkPad-T430i:/home/leichaojian# lsof -i:7777 root@ThinkPad-T430i:/home/leichaojian# cc srv.c root@ThinkPad-T430i:/home/leichaojian# ./a.out ~~~ 客户端: ~~~ leichaojian@ThinkPad-T430i:~$ ./a.out 127.0.0.1 Fri Sep 5 00:14:14 2014 ~~~ **这里lsof -i:7777是必要的,否则端口会被占用,导致connect失败!** 对于习题1.5,修改代码如下: ~~~ for ( count = 0; count < strlen( buff ); count++ ) write( connfd, &buff[ count ], 1 ); ~~~    但是客户端依旧只返回1,即虽然服务端是分字符write,但实际上还是一次性的发送所有的数据。
';

前言

最后更新于:2022-04-01 14:48:49

> 原文出处:[UNIX网络编程学习专栏](http://blog.csdn.net/column/details/unix-fzyz-sb.html) 作者:[fzyz_sb](http://blog.csdn.net/fzyz_sb) **本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!** # UNIX网络编程学习专栏 > 基于W.Richard Stevens六本著名的书籍:UNIX环境高级编程,UNIX网络编程卷1:套接字联网API和卷2:进程间通信的学习,TCP/IP详解三卷的学习,来达到入门UNIX网络编程领域。
';