TCP 客户服务器程序示例

本贴最后更新于 363 天前,其中的信息可能已经时移世易

1. 套接字设置

基本的网络应用模型如图所示:

image

客户与服务器通过套接字进行通信。套接字就像一个文件描述符,可以通过它进行读写。

==客户和服务器连接首先要设置套接字==

客户与服务器建立 TCP 连接过程:

image

1.1 客户端套接字设置


  • socket
  • connect

基本 TCP 客户/服务器程序的套接字函数

image

int sockfd; struct sockaddr_in servaddr; if (argc != 2) err_quit("usage: tcpcli <IPaddress>"); // 创建套接字 sockfd = Socket(AF_INET, SOCK_STREAM, 0); // 网络连接 bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); Inet_pton(AF_INET, argv[1], &servaddr.sin_addr); Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
  • 7: 创建套接字,返回一个描述符,用于通信
  • 9~15 行: 创建连接,主要是 connect 函数。客户端调用 Connect 函数,==用于指定要连接服务端的 IP 地址和端口号==

image

客户在调用函数connect前不必非得调用bind函数(我们在下一节介绍该函数),因为如果需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口。
  • 调用 Connect 函数将会激发三次握手1

1.2 服务端套接字设置


服务器的套接字设置分为三个步骤

  • socket
  • bind
  • listen
  • accept
int listenfd; struct sockaddr_in cliaddr, servaddr; // 第一步:socket 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(SERV_PORT); // 第二部:bind Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); // 第三步:listen Listen(listenfd, LISTENQ); for(;;){ clilen = sizeof(cliaddr); // 第四步:accept connfd = Accept(listenfd, (SA *) &cliaddr, &clilen); // 网络处理函数 ************ }

Socket 函数(第 7 行)

image

  • 表示服务端创建了一个套接字,这是必须的步骤

Bind 函数(9~14 行)

image

  • 用于指定一个端口号与 IP 地址,可以都指定,也可以都不指定
  • 对于服务器来讲,指定的 IP 地址表示:该套接字只接受那些目的地为这个 IP 地址的客户连接
  • 对于服务器来讲,指定的端口号表示:服务器采用哪个端口号服务客户端(一般来讲必须指定)

注意: 对于客户,不一定需要 Bind,到时候系统可以自动设置。客户套接字主要需要设置服务器的 IP 和端口号。

Listen 函数(14 行)

当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用connect发起连接的客户套接字。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。

image

  • 本函数通常应该在调用 socket 和 bind 这两个函数之后,并在调用 accept 函数之前调用。
  • 第二个参数规定了内核应该为相应套接字排队的最大连接个数。

第二个参数的理解

image

accept 函数(19 行)

image

  • 如果 accept 成功,那么其返回值是由内核自动生成的一个全新描述符,代表与所返回客户

    的 TCP 连接。

  • 一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命期内一直存在。

  • 内核为每个由服务器进程接受的客户连接创建一个已连接套接字(也就是说对于它的 TCP 三路握手过程已经完成)。当服务器完成对某个给定客户的服务时,相应的已连接套接字就被关闭。

TCP 客户/服务器程序


我们要编写一个完整的 TCP 客户/服务器程序示例,如图所示

image

  • 客户从标准输入接收数据,写给服务器
  • 服务器从网络读入这行文本,并回射给客户
  • 客户从网络读入这行文本,并写给标准输出

此外,我们还要讨论它的边界条件:

  • 正常启动的情况
  • 客户正常终止发生什么
  • 服务器在客户之前终止会发生什么

1. 服务器主函数

结构:

  • 服务器套接字设置:1.2 服务端套接字设置2
  • 回射函数(服务器从网络读入文本,并回射给客户)

使用了高并发技术

#include "unp.h" int main(int argc, char **argv) { // 第一部分:服务器套接字设置 int listenfd, connfd; pid_t childpid; 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(SERV_PORT); Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); Listen(listenfd, LISTENQ); // 高并发编程 for ( ; ; ) { clilen = sizeof(cliaddr); connfd = Accept(listenfd, (SA *) &cliaddr, &clilen); // 第二部分:回射函数 if ( (childpid = Fork()) == 0) { /* child process */ Close(listenfd); /* close listening socket */ str_echo(connfd); /* process the request */ exit(0); } Close(connfd); /* parent closes connected socket */ } }

代码解释3

  • 7~21 行:套接字设置,设置完成后进入循环

  • 26 行: 服务器阻塞于 Accept 调用,直到有客户完成连接。客户连接后,程序继续向下执行。

  • 29 行: 并发编程

    • 我们并不希望整个服务器被单个客户长期占用,而是希望同时服务多个客户。
    • 在子进程中,关闭 listenfd;在父进程中,关闭,connfd。因为在各自的进程中不使用它们
  • 34 行 str_echo: 在子进程中,用过回射函数处理客户请求

TCP 回射服务器程序:str_echo 函数

  • 从网络连接读取数据
  • 向客户端回射

str_echo.c

#include "unp.h" void str_echo(int sockfd) { ssize_t n; char buf[MAXLINE]; again: while ( (n = read(sockfd, buf, MAXLINE)) > 0){ Writen(sockfd, buf, n); // 向标准输出输出buf Writen(STDOUT_FILENO, buf, n); } if (n < 0 && errno == EINTR) goto again; else if (n < 0) err_sys("str_echo: read error"); }
  • 10 ~14:服务器从网络连接中读数据。然后向客户端写回(11 行)
  • 第 10 行中 read 函数没有用包裹函数,因为我们需要单独处理 read 的错误(信号中断)
  • 16 行: 如果遇到信号中断,read 函数返回并且将 errno 设为 EINTR。之后程序会跳转到 again 重新执行
  • 如果只是正常退出,比如客户端发送 FIN。那么 read 函数会读到 EOF,直接返回,并且 n<0,则退出 str_echo 函数

2. 客户端主函数

  • 客户从标准输入接收数据,写给服务器

结构:

  • 客户端套接字设置:1.1 客户端套接字设置4
  • 回射函数(服务器从网络读入文本,并回射给客户)
#include "unp.h" int main(int argc, char **argv) { int sockfd; struct sockaddr_in servaddr; if (argc != 2) err_quit("usage: tcpcli <IPaddress>"); sockfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); Inet_pton(AF_INET, argv[1], &servaddr.sin_addr); Connect(sockfd, (SA *) &servaddr, sizeof(servaddr)); str_cli(stdin, sockfd); /* do it all */ exit(0); }

代码解释5

创建套接字,装填网际网套接字地址结构

  • 9~13 创建一个 TCP 套接字,用服务器的 IP 地址和端口号装填一个网际网套接字地址结构。我们可从命令行参数取得服务器的 IP 地址,从头文件 unp.h 取得服务器的众所周知端口号(SERV_PORT)。

连接到套接字

  • 14~15 connect 建立与服务器的连接。str_cli 函数完成剩余部分的客户处理工作

客户发射函数

#include "unp.h" void str_cli(FILE *fp, int sockfd) { char sendline[MAXLINE], recvline[MAXLINE]; while (Fgets(sendline, MAXLINE, fp) != NULL) { Writen(sockfd, sendline, strlen(sendline)); if (Readline(sockfd, recvline, MAXLINE) == 0) err_quit("str_cli: server terminated prematurely"); Fputs(recvline, stdout); } }

读入一行,写到服务器

  • 6~7 fgets 读入一行文本,writen 把该行发送给服务器。

从服务器读入回射行,写到标准输出

  • 8~10 readline 从服务器读入回射行,fputs 把它写到标准输出。

返回到 main 函数

  • 11~12 当遇到文件结束符或错误时,fgets 将返回一个空指针,于是客户处理循环终止。我们的 Fgets 包裹函数检查是否发生错误,若发生则中止进程,因此 Fgets 只是在遇到文件结束符时才返回一个空指针。

3. 正常启动

查看端口号

TCP 服务器正常启动后,若是由内核自动分配的端口,可通过 getsockname 来获取端口号

** // 获取已分配的端口** getsockname(listenfd, (struct sockaddr*)&servaddr, &addr_len); printf("Port assigned by the system: %d\n", ntohs(servaddr.sin_port));

image


查看 TCP 状态 1

然后启动客户端连接,在服务器端可以查看连接状态,通过 netstat -a | grep 端口号

wzh@xjc-PowerEdge-R740:~$ netstat -a | grep 9877 tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN tcp 0 0 3448edf3d71a:9877 387a0ea8a205:64082 ESTABLISHED

image


查看 TCP 状态 2

也可以通过 ps -ef | grep 程序名称​查看服务程序,可以看到虚拟终端编号

image

我们看到虚拟终端在 pts/2 处。可以通过

ps -t ==虚拟终端号== -o pid,ppid,tty,stat,args,wchan,查看服务程序的具体状态:

image

4. 正常终止

  • 客户端按 Ctrl + D,即读取到 EOF 字符,fgets 返回一个空指针,于是 str_cli.c6函数返回。

  • 当 str_cli 返回到客户的 main 函数5时,main 通过调用 exit 终止。

  • 这导致客户 TCP 发送一个 FIN 给服务器,服务器 TCP 则以 ACK 响应,这就是 TCP 连接终止序

    列的前半部分。至此,照务器套接字处于 CLOSE_WAIT 状态,客户套接字则处于 FIN_WAIT_2

    状态

  • 当服务器 TCP 接收 FIN 时,服务器子进程阻塞于 readline 调用(图 5-3),于是 readline 返回 0。这导致 str_echo 函数返回服务器子进程的 main 函数。

  • 服务器子进程通过调用 exit 来终止(服务器子进程3)。

  • 服务器子进程中打开的所有描述符随之关闭。由子进程来关闭已连接套接字会引发 TCP 连接终止序列的最后两个分节:一个从服务器到客户的 FIN 和一个从客户到服务器的 ACK (图 2-5)。至此,连接完全终止,客户套接字进入 TIME_WAIT 状态。

image

注意

在服务器子进程终止时,给父进程发送一个 SIGCHLD 信号。这一点在本例中发生了,但是我们没有在代码中捕获该信号,而该信号的默认行为是被忽略。既然父进程未加处理,子进程于是进入僵死状态。我们可以使用 ps 命令验证这一点。

若客户端按 Ctrl + D 关闭,等于输入了 EOF 终止符,客户端会退出,服务端可以看到:

image

原来处于 wait_woken 的进程变成了僵尸进程,上图中有很多僵尸进程,因为我退出过很多客户端程序

3. 处理僵尸进程 - 通用服务器构建

主要内容

  • 处理僵尸进程

    • 处理被中断的系统调用(慢系统调用

处理僵尸进程

变成僵尸进程的原因是因为子进程死了,但是父进程觉得将来可能用得到,所以先将其状态保存下来。我们显然不愿意留存僵死进程。它们占用内核中的空间,最终可能导致我们耗尽进程资源。

僵尸进程是一个已经完成执行但仍在进程表中的进程。虽然这个进程已经不再执行任何实际的任务,但它在进程表中占据一个位置,直到父进程调用 wait()​ 或 waitpid()​ 来获取其退出状态。

处理流程

  • 子进程死后,会向父进程发送一个 SIGCHILD 信号
  • 父进程要设置信号处理函数
  • 父进程信号处理函数中调用 wait()​ 或 waitpid()​ 来获取其退出状态。

这样就不会出现僵尸进程。

image

image

选用 waitpid

假设有五个客户连接到了服务器:

image

客户终止,关闭了 5 个连接,终止了五个子进程:

image

建立一个 wait 并不能防止出现僵尸进程。问题在于:5 个信号都是在信号处理函数之前产生,而信号处理函数只能处理 1 次,而信号又不会排队。所以只能处理一个信号,其他的都是僵尸进程。

正确的解决办法是调用 waitpid 而不是 wait,图 5-11 给出了正确处理 SIGCHLD 的 sig_chld 函数。这个版本管用的原因在于:我们在一个循环内调用 waitpid,以获取所有已终止子进程的状态。我们必须指定 WNOHANG 选项,它告知 waitpid 在有尚未终止的子进程在运行时不要阻塞。

image

通用服务器编程

  • fork 子进程之前,必须编写捕获 SIGCHILD 信号的函数
  • 捕获信号时,必须处理被中断的慢系统调用
  • SIGCHILD 信号处理函数必须正确编写,防止僵尸进程

通用服务器函数(tcpserv04.c)

#include "unp.h" int main(int argc, char **argv) { int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; void sig_chld(int); 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(SERV_PORT); Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); Listen(listenfd, LISTENQ); //捕捉SIGCHILD信号 Signal(SIGCHLD, sig_chld); /* must call waitpid() */ for ( ; ; ) { clilen = sizeof(cliaddr); //捕获信号时,必须处理被中断的系统调用** if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) { if (errno == EINTR) continue; /* back to for() */ else err_sys("accept error"); } if ( (childpid = Fork()) == 0) { /* child process */ Close(listenfd); /* close listening socket */ str_echo(connfd); /* process the request */ exit(0); } Close(connfd); /* parent closes connected socket */ } }

代码解释7

服务器套接字设置

  • 3~21 行:设置套接字

捕捉信号,处理僵尸进程

  • 24 行:signal 函数捕获子进程提交的 SIGCHILD,之后进入 sig_child 函数。

处理被中断的系统调用

  • 30~35:处理被中断的系统调用8

处理被中断的系统调用

  • 我们键入 EOF 字符来终止客户。客户 TCP 发送一个 FIN 给服务器,服务器响应以一个 ACK。
  • 服务端子进程7阻塞中的 readline 读到 EOF,从而子进程终止。
  • 当 SIGCHLD 信号递交时,父进程阻塞于 accept 调用。sig_chld 函数(信号处理函数)执行,其 wait 调用取到子进程的 PID 和终止状态,随后是 printf 调用,最后返回。
  • 既然该信号是在父进程阻塞于慢系统调用(accept)时由父进程捕获的,内核就会使 accept 返回一个 EINTR 错误(被中断的系统调用) 。而父进程不处理该错误,于是中止。

因此我们需要处理被中断的系统调用 accept:

//捕获信号时,必须处理被中断的系统调用** if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) { if (errno == EINTR) continue; /* back to for() */ else err_sys("accept error"); }


  1. image <a href=↩" />

  2. 1.2 服务端套接字设置

  3. #include "unp.h" int main(int argc, char **argv) { // 第一部分:服务器套接字设置 int listenfd, connfd; pid_t childpid; 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(SERV_PORT); Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); Listen(listenfd, LISTENQ); // 高并发编程 for ( ; ; ) { clilen = sizeof(cliaddr); connfd = Accept(listenfd, (SA *) &cliaddr, &clilen); // 第二部分:回射函数 if ( (childpid = Fork()) == 0) { /* child process */ Close(listenfd); /* close listening socket */ str_echo(connfd); /* process the request */ exit(0); } Close(connfd); /* parent closes connected socket */ } }
  4. 1.1 客户端套接字设置

  5. #include "unp.h" int main(int argc, char **argv) { int sockfd; struct sockaddr_in servaddr; if (argc != 2) err_quit("usage: tcpcli <IPaddress>"); sockfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); Inet_pton(AF_INET, argv[1], &servaddr.sin_addr); Connect(sockfd, (SA *) &servaddr, sizeof(servaddr)); str_cli(stdin, sockfd); /* do it all */ exit(0); }
  6. #include "unp.h" void str_cli(FILE *fp, int sockfd) { char sendline[MAXLINE], recvline[MAXLINE]; while (Fgets(sendline, MAXLINE, fp) != NULL) { Writen(sockfd, sendline, strlen(sendline)); if (Readline(sockfd, recvline, MAXLINE) == 0) err_quit("str_cli: server terminated prematurely"); Fputs(recvline, stdout); } }
  7. #include "unp.h" int main(int argc, char **argv) { int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; void sig_chld(int); 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(SERV_PORT); Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); Listen(listenfd, LISTENQ); //捕捉SIGCHILD信号 Signal(SIGCHLD, sig_chld); /* must call waitpid() */ for ( ; ; ) { clilen = sizeof(cliaddr); //捕获信号时,必须处理被中断的系统调用** if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) { if (errno == EINTR) continue; /* back to for() */ else err_sys("accept error"); } if ( (childpid = Fork()) == 0) { /* child process */ Close(listenfd); /* close listening socket */ str_echo(connfd); /* process the request */ exit(0); } Close(connfd); /* parent closes connected socket */ } }
  8. 处理被中断的系统调用

  • TCP
    32 引用 • 38 回帖 • 2 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...