TCP 客户服务器程序示例

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
    31 引用 • 38 回帖 • 2 关注

相关帖子

欢迎来到这里!

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

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