Skip to content

LV020-TCP协议下的三种服务器

前边已经学习了最基本的TCP编程,不知道有没有发现一个问题,那就是我们的服务器端每次只能连接一个客户端,并且似乎也只能接受一次连接,这篇笔记就来介绍一下TCP编程中的三种服务器,看看这三种服务器是否是就可以解决这问题,实现并发。若笔记中有错误或者不合适的地方,欢迎批评指正😃。

这一篇笔记的所有例程都以前一篇笔记的最后一个双向传输的实例为基础进行来学习TCP的三种服务器:循环服务器、多线程服务器和多进程服务器。

一、TCP循环服务器

首先我们介绍一种TCP循环服务器,这种方式其实在TCP编程中非常少用,而且个人感觉使用起来比较麻烦,要处理的地方很多,虽然也能用吧,但是不如后面两种服务器,而且至少我还没有见过用到它的地方。

1. 服务器模型

循环服务器程序模型如下:

c
socket(...);
bind(...);
listen(...);
while(1)
{
	accept(...);
	process(...);
	close(...);
}

2. 使用实例

在此示例中,我只修改了服务器,并未修改客户端,主要目的是为了显示一下会出现的问题,来帮助理解。

2.1 server服务器端

c
/** =====================================================
 * Copyright © hk. 2022-2022. All rights reserved.
 * File name: server.c
 * Author   : fanhua
 * Description: server服务器端——TCP
 * ======================================================
 */
/* 头文件 */
#include <stdio.h>      /* perror */
#include <stdlib.h>     /* exit atoi */
#include <errno.h>     /* errno号 */
#include <sys/types.h>  /* socket           bind listen accept send */
#include <sys/socket.h> /* socket inet_addr bind listen accept send */
#include <strings.h>    /* bzero */
#include <arpa/inet.h>  /* htons  inet_addr inet_pton */
#include <netinet/in.h> /* ntohs  inet_addr inet_ntop*/
#include <unistd.h>     /* close */
#include <string.h>     /* strlen strcat*/

void usage(char *str); /* 提示信息打印函数 */

int main(int argc, char *argv[])
{
	/* 参数判断及端口号处理 */
	int port = -1;
	if (argc != 3)/* 参数数量不对时打印提示信息 */
	{
		usage(argv[0]);
		exit(-1);
	}
	port = atoi(argv[2]);/* 字符串转数字 */
	if (port < 5000)
	{
		usage(argv[0]);
		exit(-1);
	}
	/* 1.创建套接字,得到socket描述符 */
	int socket_fd = -1; /* 接收服务器端socket描述符 */
	if((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)/* SOCK_STREAM,使用TCP协议 */
	{
		perror ("socket");
		exit(-1);
	}
	/* 允许绑定地址快速重用 */
	int b_reuse = 1;
	setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
	/* 2.将套接字与指定端口号和IP进行绑定 */
	/* 2.1填充struct sockaddr_in结构体变量 */
	struct sockaddr_in sin;
	bzero (&sin, sizeof (sin));  /* 将内存块(字符串)的前n个字节清零 */
	sin.sin_family = AF_INET;    /* 协议族, IPv4 */
	sin.sin_port = htons(port);  /* 网络字节序的端口号 */
	if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* 填充IP地址,INADDR_ANY表示允许监听任意IP,但是它其实是(in_addr_t) 0x00000000 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	/* 2.2绑定 */
	if(bind(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
	{
		perror("bind");
		exit(-1);
	}
	/*3.调用listen()把主动套接字变成被动套接字 */
	if (listen(socket_fd, 5) < 0)
	{
		perror("listen");
		exit(-1);
	}
	printf ("Server starting....OK!\n");
	/*4.阻塞等待客户端连接请求 */
	int newfd = -1;
	struct sockaddr_in cin;          /* 用于存储成功连接的客户端的IP信息 */
	socklen_t addrlen = sizeof(cin);

	/* 5.打印成功连接的客户端的信息(此处只能打印一个) */
	char ipv4_addr[16];


	/* 6.数据读写 */
	int ret = -1;
	char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
	char replay[BUFSIZ];
	while(1)
	{
		/* 这一部分为原来的4和5两步,移动到这里实现循换服务器 */
		if ((newfd = accept(socket_fd, (struct sockaddr *)&cin, &addrlen)) < 0)
		{
			perror("accept");
			exit(-1);
		}
		if (!inet_ntop(AF_INET, (void *)&cin.sin_addr, ipv4_addr, sizeof(cin)))
		{
			perror ("inet_ntop");
			exit(-1);
		}
		printf ("Clinet(%s:%d) is connected successfully![newfd=%d]\n", ipv4_addr, ntohs(cin.sin_port), newfd);
		
		/* 6.接收来自客户端的数据 */
		bzero(buf, BUFSIZ);
		bzero(replay, BUFSIZ);
		do
		{
			ret = read(newfd, buf, BUFSIZ - 1);
		}
		while(ret < 0 && EINTR == errno);
		if(ret < 0)
		{
			perror("read");
			exit(-1);
		}
		if(!ret) break; /* 对方已经关闭 */
		printf("Receive data: %s\n", buf);
		/* 6.2对客户端做出应答 */
		strcat(replay, buf);
		ret = send(newfd, replay, strlen(replay), 0);
		if(ret < 0)
		{
			perror("send");
			exit(-1);
		}
		/* 6.3判断是否需要退出 */
		if(!strncasecmp(buf, "quit", strlen("quit")))  	//用户输入了quit字符
		{
			printf ("Client is exiting!\n");
			break;
		}
		close(newfd);/* 关闭这一次客户端连接的文件描述符 */
	}
	/* 7.关闭文件描述符 */
	close(socket_fd);

	return 0;
}

/**
 * @Function: usage
 * @Description: 用户提示信息打印函数
 * @param str : 当前应用程序命令字符串,一般是./app
 * @return  : none
 */
void usage(char *str)
{
	printf ("\n%s serv_ip serv_port", str);
	printf ("\n\t serv_ip: server ip address");
	printf ("\n\t serv_port: server port(>5000)\n\n");
	printf ("\n\t Attention: The IP address must be the IP address of the local nic or 0.0.0.0 \n\n");
}

2.2 client客户端

c
/** =====================================================
 * Copyright © hk. 2022-2022. All rights reserved.
 * File name: cilent.c
 * Author   : fanhua
 * Description: client客户端程序——TCP
 * ======================================================
 */
/* 头文件 */
#include <stdio.h>     /* perror fgets */
#include <stdlib.h>    /* exit atoi*/
#include <errno.h>     /* errno号 */
#include <unistd.h>    /* write close */

#include <sys/types.h> /* socket           connect send */
#include <sys/socket.h>/* socket inet_addr connect send */
#include <netinet/in.h>/* inet_addr */
#include <arpa/inet.h> /* inet_addr inet_pton htonl*/

#include <string.h>   /* bzero strncasecmp strlen */

void usage(char *str); /* 提示信息打印函数 */

/* 主函数 */
int main(int argc, char *argv[])
{
	/* 参数判断及端口号处理 */
	int port = -1;
	int portClient = -1;
	if (argc != 4)/* 参数数量不对时打印提示信息 */
	{
		usage(argv[0]);
		exit(-1);
	}
	port = atoi(argv[2]);/* 字符串转数字 */
	portClient = atoi(argv[3]);
	if (port < 5000 || portClient < 5000 || (port == portClient))
	{
		usage(argv[0]);
		exit(-1);
	}

	/* 1.打开套接字,得到套接字描述符 */
	int socket_fd = -1; /* 接收服务器端socket描述符 */
	if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		perror ("socket");
		exit(-1);
	}
	/* 允许绑定地址快速重用 */
	int b_reuse = 1;
	setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
	/* 2.连接服务器 */
	/* 2.1填充struct sockaddr_in结构体变量 */
	struct sockaddr_in sin;
	bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
	sin.sin_family = AF_INET;   /* 协议族 */
	sin.sin_port = htons(port); /* 网络字节序的端口号 */
	/* 客户端的 argv[1] 需要与系统的IP一致 */
	if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* IP地址 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	/* 绑定固定的IP 和 端口号 */
	struct sockaddr_in sinClient;
	bzero(&sinClient, sizeof (sinClient)); /* 将内存块(字符串)的前n个字节清零 */
	sinClient.sin_family = AF_INET;   /* 协议族 */
	sinClient.sin_port = htons(portClient); /* 网络字节序的端口号 */
	if(inet_pton(AF_INET, argv[1], (void *)&sinClient.sin_addr) != 1)/* IP地址 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	if(bind(socket_fd, (struct sockaddr *)&sinClient, sizeof(sinClient)) < 0)
	{
		perror("bind");
		exit(-1);
	}

	/* 2.2连接 */
	if(connect(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
	{
		perror("connect");
		exit(-1);
	}
	printf("Client staring...OK!\n");
	/* 3.数据读写 */
	int ret = -1;
	char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
	char replay[BUFSIZ];
	while (1)
	{
		/* 6.1发送数据 */
		bzero (buf, BUFSIZ);
		bzero (replay, BUFSIZ);
		printf(">");
		if (fgets(buf, BUFSIZ - 1, stdin) == NULL)
		{
			continue;
		}
		do{
			ret = write(socket_fd, buf, strlen(buf));
		}while (ret < 0 && EINTR == errno);
		/* 6.2接收服务器的回馈数据 */
		ret = recv(socket_fd, replay, BUFSIZ, 0);
		if(ret < 0)
		{
			perror("recv");
			exit(-1);
		}
		printf("server replay:%s\n", replay);
		/* 6.3判断是否需要退出 */
		if (!strncasecmp(buf, "quit", strlen ("quit")))  	//用户输入了quit字符
		{
			printf ("Client is exiting!\n");
			break;
		}
	}
	/* 4.关闭文件描述符 */
	close(socket_fd);

	return 0;
}

/**
 * @Function: usage
 * @Description: 用户提示信息打印函数
 * @param str : 当前应用程序命令字符串,一般是./app
 * @return  : none
 */
void usage(char *str)
{
	printf ("\n%s serv_ip serv_port", str);
	printf ("\n\t serv_ip: server ip address");
	printf ("\n\t serv_port: server port(>5000)\n\n");
	printf ("\n\t client_port: client portClient(>5000 && !=port )\n\n");
}

2.3 Makefile

由于需要生成两个可执行程序,自己输命令有些繁琐,这里使用make来进行。

makefile
## =====================================================
# Copyright © hk. 2022-2022. All rights reserved.
# File name: Makefile
# Author   : fanhua
# Description: Makefile文件
## ======================================================
# 
CC = gcc

DEBUG = -g -O2 -Wall
CFLAGS += $(DEBUG)

# 所有.c文件去掉后缀
TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}} 
all : $(TARGET_LIST)

%.o : %.c
	$(CC) $(CFLAGS) -c $< -o $@

.PHONY: all clean clean_o clean_out
clean : clean_o clean_out
	@rm -vf $(TARGET_LIST) 
	
clean_o :
	@rm -vf *.o
	
clean_out :
	@rm -vf *.out

2.4 测试结果

我们执行以下命令编译链接程序,生成两个可执行文件:

shell
make

然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序server和客户端程序client

shell
gcc -g -O2 -Wall    client.c   -o client
gcc -g -O2 -Wall    server.c   -o server

对于服务器端,我们执行以下命令启动服务器进程:

shell
./server 0.0.0.0 5001 # 允许监听所有网卡IP及端口

对于客户端,我们执行以下命令启动客户端进程:

shell
./client 192.168.10.101 5001 5003 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5003

然后我们就会看到如下现象:

example1

其实这个例子只是为了发现一个问题,就是我们服务器改为循环服务器后,可以持续监听,等待客户端的连接,但是每次却只能接收一个数据,这是因为我们服务器在接收一次数据后吗,就将客户端连接过来的新的socket描述符关闭了,想要一个客户端也可以持续发送数据我们其实将客户端的connect()函数放在循环中就可以了,但是这样显得就很麻烦。我们若是再开启一个终端,输入以下命令:

shell
./client 192.168.10.101 5001 5004 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5004, 不能与上边冲突

然后会发现,有下边两种情况,这里就不放图了,可以自己试一下:

  • 第一种,两个客户端都连接上去,先连接的客户端先发送数据

这种情况下,先连接的客户端只能发送接收一次数据,然后后连接的也是一样,可以正常连接并且发送一次数据。

  • 第一种,两个客户端都连接上去,后连接的客户端先发送数据

这种情况下,后连接的客户端连接时,虽然连接成功了,但是服务器端不会有任何反反应,因为它等待着先连接的客户端发送数据,当后连接的客户端先发送数据时,服务器端也不会有任何反应,当先连接的客户端发送一次数据后,后连接的客户端的数据也在服务器端显示了。

毫无疑问,这样的程序是很不严谨的,因为每一次我们将数据发过去后,都会导致需要重新连接,当然这也可以实现服务器的持续监听,但是不免有很多的问题,所以一般情况不会在TCP编程中使用这种服务器。

二、TCP多线程服务器

1. 服务器模型

c
socket(...);
bind(...);
listen(...);
while(1)
{
	accpet(...);
	if((pthread_create(...))!==-1)
	{
		process(...);
		close(...);
		exit(...);
	}
	close(...);
}

【注意】

(1)编译程序的时候一定要加上-lpthread

(2)注意线程结束后,线程的回收问题,可以设置一个线程分离,这样线程结束后就会自动回收。

2. 使用实例

在此示例中,我只修改了服务器,并未修改客户端。需要注意的是,我们在线程执行函数中一定要设置线程分离,以在该线程结束的时候自动回收资源,防止出现僵尸线程。

2.1 server服务器端

c
/** =====================================================
 * Copyright © hk. 2022-2022. All rights reserved.
 * File name: server.c
 * Author   : fanhua
 * Description: server服务器端——TCP
 * ======================================================
 */
/* 头文件 */
#include <stdio.h>      /* perror */
#include <stdlib.h>     /* exit atoi */
#include <errno.h>      /* errno号 */
#include <sys/types.h>  /* socket           bind listen accept send */
#include <sys/socket.h> /* socket inet_addr bind listen accept send */
#include <strings.h>    /* bzero */
#include <arpa/inet.h>  /* htons  inet_addr inet_pton */
#include <netinet/in.h> /* ntohs  inet_addr inet_ntop*/
#include <unistd.h>     /* close */
#include <string.h>     /* strlen strcat*/
#include <pthread.h>    /* pthread_create pthread_detach pthread_self */

void usage(char *str); /* 提示信息打印函数 */
void *clientDataHandle(void *arg);/* 线程处理函数 */
int main(int argc, char *argv[])
{
	/* 参数判断及端口号处理 */
	int port = -1;
	if (argc != 3)/* 参数数量不对时打印提示信息 */
	{
		usage(argv[0]);
		exit(-1);
	}
	port = atoi(argv[2]);/* 字符串转数字 */
	if (port < 5000)
	{
		usage(argv[0]);
		exit(-1);
	}
	/* 1.创建套接字,得到socket描述符 */
	int socket_fd = -1; /* 接收服务器端socket描述符 */
	if((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)/* SOCK_STREAM,使用TCP协议 */
	{
		perror ("socket");
		exit(-1);
	}
	/* 允许绑定地址快速重用 */
	int b_reuse = 1;
	setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
	/* 2.将套接字与指定端口号和IP进行绑定 */
	/* 2.1填充struct sockaddr_in结构体变量 */
	struct sockaddr_in sin;
	bzero (&sin, sizeof (sin));  /* 将内存块(字符串)的前n个字节清零 */
	sin.sin_family = AF_INET;    /* 协议族, IPv4 */
	sin.sin_port = htons(port);  /* 网络字节序的端口号 */
	if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* 填充IP地址,INADDR_ANY表示允许监听任意IP,但是它其实是(in_addr_t) 0x00000000 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	/* 2.2绑定 */
	if(bind(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
	{
		perror("bind");
		exit(-1);
	}
	/*3.调用listen()把主动套接字变成被动套接字 */
	if (listen(socket_fd, 5) < 0)
	{
		perror("listen");
		exit(-1);
	}
	printf ("Server starting....OK!\n");
	/*4.阻塞等待客户端连接请求所需变量定义 */
	int newfd = -1;
	struct sockaddr_in cin;          /* 用于存储成功连接的客户端的IP信息 */
	socklen_t addrlen = sizeof(cin);	
	/* 5.打印成功连接的客户端的信息相关变量定义 */
	char ipv4_addr[16];
	/* 6.多线程相关变量定义 */
	pthread_t tid;
	while(1)
	{
		/* 等待连接 */
		if ((newfd = accept(socket_fd, (struct sockaddr *)&cin, &addrlen)) < 0)
		{
			perror("accept");
			exit(-1);
		}
		/* 获取连接成功的客户端信息 */
		if (!inet_ntop(AF_INET, (void *)&cin.sin_addr, ipv4_addr, sizeof(cin)))
		{
			perror ("inet_ntop");
			exit(-1);
		}
		printf ("Clinet(%s:%d) is connected successfully![newfd=%d]\n", ipv4_addr, ntohs(cin.sin_port), newfd);
		/* 创建线程处理客户端请求 */
		pthread_create (&tid, NULL, clientDataHandle, (void *) &newfd);
	}
	/* 7.关闭文件描述符 */
	close(socket_fd);

	return 0;
}

/**
 * @Function: usage
 * @Description: 用户提示信息打印函数
 * @param str : 当前应用程序命令字符串,一般是./app
 * @return  : none
 */
void usage(char *str)
{
	printf ("\n%s serv_ip serv_port", str);
	printf ("\n\t serv_ip: server ip address");
	printf ("\n\t serv_port: server port(>5000)\n\n");
	printf ("\n\t Attention: The IP address must be the IP address of the local nic or 0.0.0.0 \n\n");
}

/**
 * @Function: clientDataHandle
 * @Description: 多线程的线程开始函数,此函数中完成客户端数据读取
 * @param arg: 线程创建时,主线程传递给线程的参数
 * @return  : 返回一个 void *类型的数据
 */
void *clientDataHandle(void *arg)
{
	int newfd = *(int *) arg;
	int ret = -1;
	char buf[BUFSIZ];
	char replay[BUFSIZ];
	printf ("handler thread: newfd =%d\n", newfd);
	/* 设置线程分离,线程结束后自动回收 */
	pthread_detach(pthread_self());
	/* 数据读写 */
	while (1)
	{
		bzero(buf, BUFSIZ);
		bzero(replay, BUFSIZ);
		/* 读取客户端数据 */
		do
		{
			ret = read(newfd, buf, BUFSIZ-1);
		}while (ret < 0 && EINTR == errno);
		if(ret < 0)
		{
			perror ("read");
			exit(-1);
		}
		if(!ret) break;/* 客户端已经关闭 */
		printf ("Receive client[%d] data: %s\n", newfd, buf);
		/* 对客户端做出回应 */
		strcat(replay, buf);
		ret = send(newfd, replay, strlen(replay), 0);
		if(ret < 0)
		{
			perror("send");
			exit(-1);
		}
		/* 判断是否需要退出 */
		if(!strncasecmp(buf, "quit", strlen("quit")))  	//用户输入了quit字符
		{
			printf ("Client[%d] is exiting!\n", newfd);
			break;
		}
	}
	/* 6.关闭文件描述符 */
	close(newfd);
	return (void *)0;
}

2.2 client客户端

c
/** =====================================================
 * Copyright © hk. 2022-2022. All rights reserved.
 * File name: cilent.c
 * Author   : fanhua
 * Description: client客户端程序——TCP
 * ======================================================
 */
/* 头文件 */
#include <stdio.h>     /* perror fgets */
#include <stdlib.h>    /* exit atoi*/
#include <errno.h>     /* errno号 */
#include <unistd.h>    /* write close */

#include <sys/types.h> /* socket           connect send */
#include <sys/socket.h>/* socket inet_addr connect send */
#include <netinet/in.h>/* inet_addr */
#include <arpa/inet.h> /* inet_addr inet_pton htonl*/

#include <string.h>   /* bzero strncasecmp strlen */

void usage(char *str); /* 提示信息打印函数 */

/* 主函数 */
int main(int argc, char *argv[])
{
	/* 参数判断及端口号处理 */
	int port = -1;
	int portClient = -1;
	if (argc != 4)/* 参数数量不对时打印提示信息 */
	{
		usage(argv[0]);
		exit(-1);
	}
	port = atoi(argv[2]);/* 字符串转数字 */
	portClient = atoi(argv[3]);
	if (port < 5000 || portClient < 5000 || (port == portClient))
	{
		usage(argv[0]);
		exit(-1);
	}

	/* 1.打开套接字,得到套接字描述符 */
	int socket_fd = -1; /* 接收服务器端socket描述符 */
	if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		perror ("socket");
		exit(-1);
	}
	/* 允许绑定地址快速重用 */
	int b_reuse = 1;
	setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
	/* 2.连接服务器 */
	/* 2.1填充struct sockaddr_in结构体变量 */
	struct sockaddr_in sin;
	bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
	sin.sin_family = AF_INET;   /* 协议族 */
	sin.sin_port = htons(port); /* 网络字节序的端口号 */
	/* 客户端的 argv[1] 需要与系统的IP一致 */
	if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* IP地址 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	/* 绑定固定的IP 和 端口号 */
	struct sockaddr_in sinClient;
	bzero(&sinClient, sizeof (sinClient)); /* 将内存块(字符串)的前n个字节清零 */
	sinClient.sin_family = AF_INET;   /* 协议族 */
	sinClient.sin_port = htons(portClient); /* 网络字节序的端口号 */
	if(inet_pton(AF_INET, argv[1], (void *)&sinClient.sin_addr) != 1)/* IP地址 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	if(bind(socket_fd, (struct sockaddr *)&sinClient, sizeof(sinClient)) < 0)
	{
		perror("bind");
		exit(-1);
	}

	/* 2.2连接 */
	if(connect(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
	{
		perror("connect");
		exit(-1);
	}
	printf("Client staring...OK!\n");
	/* 3.数据读写 */
	int ret = -1;
	char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
	char replay[BUFSIZ];
	while (1)
	{
		/* 6.1发送数据 */
		bzero (buf, BUFSIZ);
		bzero (replay, BUFSIZ);
		printf(">");
		if (fgets(buf, BUFSIZ - 1, stdin) == NULL)
		{
			continue;
		}
		do{
			ret = write(socket_fd, buf, strlen(buf));
		}while (ret < 0 && EINTR == errno);
		/* 6.2接收服务器的回馈数据 */
		ret = recv(socket_fd, replay, BUFSIZ, 0);
		if(ret < 0)
		{
			perror("recv");
			exit(-1);
		}
		printf("server replay:%s\n", replay);
		/* 6.3判断是否需要退出 */
		if (!strncasecmp(buf, "quit", strlen ("quit")))  	//用户输入了quit字符
		{
			printf ("Client is exiting!\n");
			break;
		}
	}
	/* 4.关闭文件描述符 */
	close(socket_fd);

	return 0;
}

/**
 * @Function: usage
 * @Description: 用户提示信息打印函数
 * @param str : 当前应用程序命令字符串,一般是./app
 * @return  : none
 */
void usage(char *str)
{
	printf ("\n%s serv_ip serv_port", str);
	printf ("\n\t serv_ip: server ip address");
	printf ("\n\t serv_port: server port(>5000)\n\n");
	printf ("\n\t client_port: client portClient(>5000 && !=port )\n\n");
}

2.3 Makefile

由于需要生成两个可执行程序,自己输命令有些繁琐,这里使用make来进行。这一次由于需要用到线程库pthread.h所以我们在链接的时候需要加上-lpthread参数,由于是一个Makefile生成两个可执行程序,所以这里我们使用隐含规则推导的话,需要将编译和链接的步骤分开,所以其实下边的程序其实并不是很通用。

注意】为了使用ps命令时可以更清楚的看到现象,所以这里服务器端生成的可执行程序名称我改成了myserver

makefile
## =====================================================
# Copyright © hk. 2022-2022. All rights reserved.
# File name: Makefile
# Author   : fanhua
# Description: Makefile文件
## ======================================================
# 
TARGET1=myserver
TARGET2=client
CC = gcc

DEBUG = -g -O2 -Wall
CFLAGS += $(DEBUG)

# 所有.c文件去掉后缀
TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}} 
OBJ_LIST = ${patsubst %.c, %.o, ${wildcard *.c}} 
all : $(TARGET1) $(TARGET2)
	
$(TARGET1): server.o
	$(CC) $(CFLAGS) $< -o $@ -lpthread

$(TARGET2): client.o
	$(CC) $(CFLAGS) $< -o $@ -lpthread

$(OBJ_LIST): %.o : %.c
	$(CC) $(CFLAGS) -c $< -o $@

.PHONY: all clean clean_o clean_out
clean : clean_o clean_out
	@rm -vf $(TARGET_LIST) $(TARGET1) $(TARGET2) 
	
clean_o :
	@rm -vf *.o
	
clean_out :
	@rm -vf *.out

2.4 测试结果

我们执行以下命令编译链接程序,生成两个可执行文件:

shell
make

然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序myserver和客户端程序client

shell
gcc -g -O2 -Wall -c server.c -o server.o
gcc -g -O2 -Wall server.o -o myserver -lpthread
gcc -g -O2 -Wall -c client.c -o client.o
gcc -g -O2 -Wall client.o -o client -lpthread

对于服务器端,我们执行以下命令启动服务器进程:

shell
./myserver 0.0.0.0 5001 # 允许监听所有网卡IP及端口

对于客户端,我们分别在三个终端中执行以下命令启动三个客户端进程:

shell
./client 192.168.10.101 5001 5002 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5002
./client 192.168.10.101 5001 5003 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5003
./client 192.168.10.101 5001 5004 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5004

然后我们会看到如下运行结果:

image-20220629153731889

我们通过以下命令查看myserver服务器端所有线程:

shell
ps -eLf|grep myserver

然后便会有以下提示信息:

image-20220629154149028

上图中的(1)是刚才连接了三个客户端的情况,一共有四个myserver线程,后边我们在客户端进程输入quit以退出客户端,每退出一个客户端进行就会减少一个线程。每增加一个客户端,就会增加一个线程。每个客户端都是独立正常工作的,其实就达到了一种并发的现象,使得我们的服务器可以同时处理多个客户端的请求。

三、TCP多进程服务器

1. 服务器模型

c
socket(...);
bind(...);
listen(...);
while(1)
{
	accpet(...);
	if(fork(...) == 0)
	{
		process(...);
		close(...);
		exit(...);
	}
	close(...);
}

【注意】

(1)进程结束后如果不回收的话,会产生僵尸进程,我们为了实现自动回收,可以使用信号来进行操作。

(2)同一个进程打开不同的文件的时候,文件描述符会递增。

(3)不同的进程打开同一个文件,它们得到的文件描述符可能是一致的。

(4)不同的进程打开不同的文件,它们得到的文件描述符也有可能是一致的。

后边的(2)、(3)和(4)几条结论是后来自己测试出来的,其实也不难理解,就是进程都有自己独自的虚拟内存空间,是互不影响的。

2. 使用实例

在此示例中,我只修改了服务器,并未修改客户端。需要注意的是,我们在服务器端要对结束的子进程进行回收,由于子进程结束的时候会向父进程发出SIGCHLD信号,所以我们可以使用信号来完成子进程的回收,以避免出现僵尸进程。

2.1 server服务器端

c
/** =====================================================
 * Copyright © hk. 2022-2022. All rights reserved.
 * File name: server.c
 * Author   : fanhua
 * Description: server服务器端——TCP
 * ======================================================
 */
/* 头文件 */
#include <stdio.h>      /* perror */
#include <stdlib.h>     /* exit atoi */
#include <errno.h>      /* errno号 */
#include <sys/types.h>  /* socket           bind listen accept send fork */
#include <sys/socket.h> /* socket inet_addr bind listen accept send */
#include <strings.h>    /* bzero */
#include <arpa/inet.h>  /* htons  inet_addr inet_pton */
#include <netinet/in.h> /* ntohs  inet_addr inet_ntop*/
#include <unistd.h>     /* close fork */
#include <string.h>     /* strlen strcat*/
#include <signal.h>    /* signal */
#include <sys/wait.h>  /* waitpid */

void usage(char *str); /* 提示信息打印函数 */
void clientDataHandle(void *arg);/* 进程处理函数 */
void sigChildHandle(int signo);  /* 信号回收子进程 */
int main(int argc, char *argv[])
{
	/* 参数判断及端口号处理 */
	int port = -1;
	if (argc != 3)/* 参数数量不对时打印提示信息 */
	{
		usage(argv[0]);
		exit(-1);
	}
	port = atoi(argv[2]);/* 字符串转数字 */
	if (port < 5000)
	{
		usage(argv[0]);
		exit(-1);
	}
	/* 注册信号处理函数,实现进程结束自动回收 */
	signal(SIGCHLD, sigChildHandle);
	/* 1.创建套接字,得到socket描述符 */
	int socket_fd = -1; /* 接收服务器端socket描述符 */
	if((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)/* SOCK_STREAM,使用TCP协议 */
	{
		perror ("socket");
		exit(-1);
	}
	/* 允许绑定地址快速重用 */
	int b_reuse = 1;
	setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
	/* 2.将套接字与指定端口号和IP进行绑定 */
	/* 2.1填充struct sockaddr_in结构体变量 */
	struct sockaddr_in sin;
	bzero (&sin, sizeof (sin));  /* 将内存块(字符串)的前n个字节清零 */
	sin.sin_family = AF_INET;    /* 协议族, IPv4 */
	sin.sin_port = htons(port);  /* 网络字节序的端口号 */
	if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* 填充IP地址,INADDR_ANY表示允许监听任意IP,但是它其实是(in_addr_t) 0x00000000 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	/* 2.2绑定 */
	if(bind(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
	{
		perror("bind");
		exit(-1);
	}
	/*3.调用listen()把主动套接字变成被动套接字 */
	if (listen(socket_fd, 5) < 0)
	{
		perror("listen");
		exit(-1);
	}
	printf ("Server starting....OK!\n");
	/*4.阻塞等待客户端连接请求所需变量定义 */
	int newfd = -1;
	struct sockaddr_in cin;          /* 用于存储成功连接的客户端的IP信息 */
	socklen_t addrlen = sizeof(cin);	
	/* 5.打印成功连接的客户端的信息相关变量定义 */
	char ipv4_addr[16];
	/* 6.多进程程相关变量定义 */
	pid_t pid = -1;
	while(1)
	{
		/* 等待连接 */
		if ((newfd = accept(socket_fd, (struct sockaddr *)&cin, &addrlen)) < 0)
		{
			perror("accept");
			exit(-1);
		}
		/* 创建进程处理客户端请求 */
		pid = fork();
		if(pid < 0)/* 进程创建失败 */
		{
			perror("fork");
			break;
		}
		else if(pid > 0)/* 父进程 */
		{
			printf("%d\n", newfd);
			close(newfd);/* 子进程会复制父进程的资源包括文件描述符,所以父进程这里不再需要这个新的socket描述符 */
		}
		else /* 子进程,这里会复制之前成功连接到服务器的客户端而产生的的新的newfd */
		{
			close(socket_fd);/* 子进程用于处理客户端请求,所以不需要用于监听的那个socket套接字了 */
			/* 获取连接成功的客户端信息 */
			if (!inet_ntop(AF_INET, (void *)&cin.sin_addr, ipv4_addr, sizeof(cin)))
			{
				perror ("inet_ntop");
				exit(-1);
			}
			printf ("Clinet(%s:%d) is connected successfully![newfd=%d]\n", ipv4_addr, ntohs(cin.sin_port), newfd);
			clientDataHandle(&newfd);/* 处理数据 */
			exit(0);	
		}
	}
	/* 7.关闭文件描述符 */
	close(socket_fd);

	return 0;
}

/**
 * @Function: usage
 * @Description: 用户提示信息打印函数
 * @param str : 当前应用程序命令字符串,一般是./app
 * @return  : none
 */
void usage(char *str)
{
	printf ("\n%s serv_ip serv_port", str);
	printf ("\n\t serv_ip: server ip address");
	printf ("\n\t serv_port: server port(>5000)\n\n");
	printf ("\n\t Attention: The IP address must be the IP address of the local nic or 0.0.0.0 \n\n");
}

/**
 * @Function: clientDataHandle
 * @Description: 子进程处理数据函数
 * @param arg: 得到客户端连接产生新的套接字文件描述符
 * @return  : none
 */
void clientDataHandle(void *arg)
{
	int newfd = *(int *) arg;
	int ret = -1;
	char buf[BUFSIZ];
	char replay[BUFSIZ];
	printf ("handler process: newfd =%d\n", newfd);
	/* 数据读写 */
	while (1)
	{
		bzero(buf, BUFSIZ);
		bzero(replay, BUFSIZ);
		/* 读取客户端数据 */
		do
		{
			ret = read(newfd, buf, BUFSIZ-1);
		}while (ret < 0 && EINTR == errno);
		if(ret < 0)
		{
			perror ("read");
			exit(-1);
		}
		if(!ret) break;/* 客户端已经关闭 */
		printf ("Receive client[%d] data: %s\n", newfd, buf);
		/* 对客户端做出回应 */
		strcat(replay, buf);
		ret = send(newfd, replay, strlen(replay), 0);
		if(ret < 0)
		{
			perror("send");
			exit(-1);
		}
		/* 判断是否需要退出 */
		if(!strncasecmp(buf, "quit", strlen("quit")))  	//用户输入了quit字符
		{
			printf ("Client[%d] is exiting!\n", newfd);
			break;
		}
	}
	/* 6.关闭文件描述符 */
	close(newfd);
}

/**
 * @Function: sigChildHandle
 * @Description: 通过信号回收子进程
 * @param signo: 检测到的信号(子进程结束时会发出SIGCHLD信号)
 * @return  : none
 */
void sigChildHandle(int signo)
{
	if(signo == SIGCHLD)
	{
		waitpid(-1, NULL,  WNOHANG);
	}
}

2.2 client客户端

c
/** =====================================================
 * Copyright © hk. 2022-2022. All rights reserved.
 * File name: cilent.c
 * Author   : fanhua
 * Description: client客户端程序——TCP
 * ======================================================
 */
/* 头文件 */
#include <stdio.h>     /* perror fgets */
#include <stdlib.h>    /* exit atoi*/
#include <errno.h>     /* errno号 */
#include <unistd.h>    /* write close */

#include <sys/types.h> /* socket           connect send */
#include <sys/socket.h>/* socket inet_addr connect send */
#include <netinet/in.h>/* inet_addr */
#include <arpa/inet.h> /* inet_addr inet_pton htonl*/

#include <string.h>   /* bzero strncasecmp strlen */

void usage(char *str); /* 提示信息打印函数 */

/* 主函数 */
int main(int argc, char *argv[])
{
	/* 参数判断及端口号处理 */
	int port = -1;
	int portClient = -1;
	if (argc != 4)/* 参数数量不对时打印提示信息 */
	{
		usage(argv[0]);
		exit(-1);
	}
	port = atoi(argv[2]);/* 字符串转数字 */
	portClient = atoi(argv[3]);
	if (port < 5000 || portClient < 5000 || (port == portClient))
	{
		usage(argv[0]);
		exit(-1);
	}

	/* 1.打开套接字,得到套接字描述符 */
	int socket_fd = -1; /* 接收服务器端socket描述符 */
	if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		perror ("socket");
		exit(-1);
	}
	/* 允许绑定地址快速重用 */
	int b_reuse = 1;
	setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
	/* 2.连接服务器 */
	/* 2.1填充struct sockaddr_in结构体变量 */
	struct sockaddr_in sin;
	bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
	sin.sin_family = AF_INET;   /* 协议族 */
	sin.sin_port = htons(port); /* 网络字节序的端口号 */
	/* 客户端的 argv[1] 需要与系统的IP一致 */
	if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* IP地址 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	/* 绑定固定的IP 和 端口号 */
	struct sockaddr_in sinClient;
	bzero(&sinClient, sizeof (sinClient)); /* 将内存块(字符串)的前n个字节清零 */
	sinClient.sin_family = AF_INET;   /* 协议族 */
	sinClient.sin_port = htons(portClient); /* 网络字节序的端口号 */
	if(inet_pton(AF_INET, argv[1], (void *)&sinClient.sin_addr) != 1)/* IP地址 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	if(bind(socket_fd, (struct sockaddr *)&sinClient, sizeof(sinClient)) < 0)
	{
		perror("bind");
		exit(-1);
	}

	/* 2.2连接 */
	if(connect(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
	{
		perror("connect");
		exit(-1);
	}
	printf("Client staring...OK!\n");
	/* 3.数据读写 */
	int ret = -1;
	char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
	char replay[BUFSIZ];
	while (1)
	{
		/* 6.1发送数据 */
		bzero (buf, BUFSIZ);
		bzero (replay, BUFSIZ);
		printf(">");
		if (fgets(buf, BUFSIZ - 1, stdin) == NULL)
		{
			continue;
		}
		do{
			ret = write(socket_fd, buf, strlen(buf));
		}while (ret < 0 && EINTR == errno);
		/* 6.2接收服务器的回馈数据 */
		ret = recv(socket_fd, replay, BUFSIZ, 0);
		if(ret < 0)
		{
			perror("recv");
			exit(-1);
		}
		printf("server replay:%s\n", replay);
		/* 6.3判断是否需要退出 */
		if (!strncasecmp(buf, "quit", strlen ("quit")))  	//用户输入了quit字符
		{
			printf ("Client is exiting!\n");
			break;
		}
	}
	/* 4.关闭文件描述符 */
	close(socket_fd);

	return 0;
}

/**
 * @Function: usage
 * @Description: 用户提示信息打印函数
 * @param str : 当前应用程序命令字符串,一般是./app
 * @return  : none
 */
void usage(char *str)
{
	printf ("\n%s serv_ip serv_port", str);
	printf ("\n\t serv_ip: server ip address");
	printf ("\n\t serv_port: server port(>5000)\n\n");
	printf ("\n\t client_port: client portClient(>5000 && !=port )\n\n");
}

2.3 Makefile

注意】为了使用ps命令时可以更清楚的看到现象,所以这里服务器端生成的可执行程序名称我改成了myserver

makefile
## =====================================================
# Copyright © hk. 2022-2022. All rights reserved.
# File name: Makefile
# Author   : fanhua
# Description: Makefile文件
## ======================================================
# 
TARGET1=myserver
TARGET2=client
CC = gcc

DEBUG = -g -O2 -Wall
CFLAGS += $(DEBUG)

# 所有.c文件去掉后缀
TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}} 
OBJ_LIST = ${patsubst %.c, %.o, ${wildcard *.c}} 
all : $(TARGET1) $(TARGET2)
	
$(TARGET1): server.o
	$(CC) $(CFLAGS) $< -o $@ -lpthread

$(TARGET2): client.o
	$(CC) $(CFLAGS) $< -o $@ -lpthread

$(OBJ_LIST): %.o : %.c
	$(CC) $(CFLAGS) -c $< -o $@

.PHONY: all clean clean_o clean_out
clean : clean_o clean_out
	@rm -vf $(TARGET_LIST) $(TARGET1) $(TARGET2) 
	
clean_o :
	@rm -vf *.o
	
clean_out :
	@rm -vf *.out

2.4 测试结果

我们执行以下命令编译链接程序,生成两个可执行文件:

shell
make

然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序myserver和客户端程序client

shell
gcc -g -O2 -Wall -c server.c -o server.o
gcc -g -O2 -Wall server.o -o myserver -lpthread
gcc -g -O2 -Wall -c client.c -o client.o
gcc -g -O2 -Wall client.o -o client -lpthread

对于服务器端,我们执行以下命令启动服务器进程:

shell
./myserver 0.0.0.0 5001 # 允许监听所有网卡IP及端口

对于客户端,我们分别在三个终端中执行以下命令启动三个客户端进程:

shell
./client 192.168.10.101 5001 5002 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5002
./client 192.168.10.101 5001 5003 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5003
./client 192.168.10.101 5001 5004 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5004

然后我们会看到如下运行结果:

image-20220629174158319

【说明】

可以看出,新的客户端连接到服务器的时候产生的新的socket描述符都是4,这是因为我们在父进程中将newfd关闭了,当我们父进程中注释掉关闭newfd这个语句的时候,就会发现每次打印的newfd是不一样的了。即便是每次获取到了相同的文件描述符数字,但是其实那都是不同的子进程中的文件描述符,即便一样,也不会影响通信。

就比如我们的标准输入输出,我们开了好几个终端,终端也是进程,他们都用到了012三个文件描述符,但是不同的终端之间并没有因为文件描述符相同而产生的混乱,这就是因为不同的进程在内存中使用的是虚拟内存空间,是相互独立互不影响的。

我们通过以下命令查看myserver服务器端所有进程:

shell
ps -elf|grep myserver

然后便会有以下提示信息:

image-20220629174424461

上图中其实与多线程的情况类似,这里就不分析了。

3. 一道题目

题目】 阅读以下程序,完成下面的题目:

c
listenfd = socket(…);
bind(listenfd,…);
listen(listenfd,…);

for ( ; ; )
{
	connfd = accept(listenfd, …);
	if (( pid = fork( )) == 0)
	{
		recv(connfd,…);
		send(connfd,…);
	}
	else
		close(connfd);
}

根据上面的程序,对以下说法判断对错

a. 这是一个并发服务器( )

b. 在任何时候,该服务器只能处理一个客户端的请求( )

c. 随着服务器端接受越来越多的请求,connfd的值变得越来越大( )

解析

a.正确,显然是一个多进程的并发服务器;

b.错误,这是一个多进程的并发服务器,可以同时在不同的进程中处理不同客户端的请求;

c.错误,由于在子进程之外,关闭了accept得到的用于数据传输的新的socket文件描述符,再由上边使用实例可知,这里的connfd是不变的。若是去掉close(connfd);是这条语句的话,connfd的值将会随着服务器端接受越来越多的客户端请求而变得越来越大。