实现迭代回声服务器端/客户端

1.迭代服务器端/客户端

之前讨论的 HelloWorld 服务器端处理完 1 1 1 个客户端连接请求即退出,连接请求等待队列实际没有太大意义。但这并非我们想象的服务器端。设置好等待队列的大小后,应向所有客户端提供服务。如果想继续受理后续的客户端连接请求,应怎样扩展代码?最简单的办法就是插入循环语句反复调用 accept 函数,如下图所示。

在这里插入图片描述

从上图可以看出,调用 accept 函数后,紧接着调用 I/O 相关的 read、write 函数,然后调用 close 函数。这并非针对服务器端套接字,而是针对 accept 函数调用时创建的套接字。

调用 close 函数就意味着结束了针对某一客户端的服务。此时如果还想服务于其他客户端,就要重新调用 accept 函数。同一时刻确实只能服务于一个客户端。将来学完进程和线程后,就可以编写同时服务多个客户端的服务器端了。目前只能做到这一步,虽然很遗憾,但请各位不要心急。

即使服务器端以迭代方式运转,客户端代码亦无太大区别。

2.迭代回声服务器端/客户端

回声(echo)服务器端/客户端,顾名思义,服务器端将客户端传输的字符串数据原封不动地传回客户端,就像回声一样。

  • 服务器端在同一时刻只与一个客户端相连,并提供回声服务。
  • 服务器端依次向 5 5 5 个客户端提供服务并退出。
  • 客户端接收用户输入的字符串并发送到服务器端。
  • 服务器端将接收的字符串数据传回客户端,即“回声”。
  • 服务器端与客户端之间的字符串回声一直执行到客户端输入 Q 为止。

2.1 echo_server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling(char *message);

int main(int argc, char *argv[])
{
    
    
	int serv_sock, clnt_sock;
	char message[BUF_SIZE];
	int str_len, i;
	
	struct sockaddr_in serv_adr;
	struct sockaddr_in clnt_adr;
	socklen_t clnt_adr_sz;
	
	if (argc != 2)
	{
    
    
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
	
	serv_sock = socket(PF_INET, SOCK_STREAM, 0);

	if (serv_sock == -1)
	{
    
    
		error_handling("socket() error");
	}
	
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family = AF_INET;
	serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_adr.sin_port = htons(atoi(argv[1]));

	if (bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1)
	{
    
    
		error_handling("bind() error");
	}
	
	if (listen(serv_sock, 5) == -1)
	{
    
    
		error_handling("listen() error");
	}
	
	clnt_adr_sz = sizeof(clnt_adr);

	// 为处理5个客户端连接而添加的循环语句。共调用5次accept函数,依次向5个客户端提供服务。
	for (i = 0; i < 5; i++)
	{
    
    
		clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_adr, &clnt_adr_sz);

		if (clnt_sock == -1)
			error_handling("accept() error");
		else
			printf("Connected client %d\n", i + 1);
		
		// 实际完成回声服务的代码,原封不动地传输读取的字符串。
		while ((str_len = read(clnt_sock, message, BUF_SIZE)) != 0)
		{
    
    
			write(clnt_sock, message, str_len);
		}

		// 针对套接字调用close函数,向连接的相应套接字发送EOF。
		// 换言之,客户端套接字若调用close函数,则第62行的循环条件变成假(false),因此执行第69行的代码。
		close(clnt_sock);
	}

	// 向5个客户端提供服务后关闭服务器端套接字并终止程序。
	close(serv_sock);

	return 0;
}

void error_handling(char *message)
{
    
    
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

编译运行:

gcc echo_server.c -o eserver
./eserver 9190

2.2 echo_client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling(char *message);

int main(int argc, char *argv[])
{
    
    
	int sock;
	char message[BUF_SIZE];
	int str_len;
	struct sockaddr_in serv_adr;

	if (argc != 3)
	{
    
    
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	
	sock = socket(PF_INET, SOCK_STREAM, 0);

	if (sock == -1)
	{
    
    
		error_handling("socket() error");
	}
	
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family = AF_INET;
	serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
	serv_adr.sin_port = htons(atoi(argv[2]));
	
	// 调用connect函数。若调用该函数引起的连接请求被注册到服务器端等待队列,则connect函数将完成正常调用。
	// 因此,即使通过第41行代码输出了连接提示字符串,如果服务器尚未调用accept函数,也不会真正建立服务关系。
	if (connect(sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1)
		error_handling("connect() error!");
	else
		puts("Connected...........");
	
	while (1)
	{
    
    
		fputs("Input message(Q to quit): ", stdout);
		fgets(message, BUF_SIZE, stdin);
		
		if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) break;

		write(sock, message, strlen(message));
		str_len = read(sock, message, BUF_SIZE - 1);
		message[str_len] = 0;
		printf("Message from server: %s", message);
	}
	
	// 调用close函数向相应套接字发送EOF(EOF即意味着中断连接)
	close(sock);

	return 0;
}

void error_handling(char *message)
{
    
    
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

编译运行:

gcc echo_client.c -o eclient
./eclient 127.0.0.1 9190

3.回声客户端存在的问题

while (1)
{
    
    
	fputs("Input message(Q to quit): ", stdout);
	fgets(message, BUF_SIZE, stdin);
	
	if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) break;

	write(sock, message, strlen(message));
	str_len = read(sock, message, BUF_SIZE - 1);
	message[str_len] = 0;
	printf("Message from server: %s", message);
}

以上代码有个错误假设:“每次调用 read、write 函数时都会以字符串为单位执行实际的 I/O 操作”,当然,每次调用 write 函数都会传递 1 1 1 个字符串,因此这种假设在某种程度上也算合理。

但“TCP不存在数据边界”,上述客户端是基于TCP的,因此,多次调用 write 函数传递的字符串有可能一次性传递到服务器端。此时客户端有可能从服务器端收到多个字符串,这不是我们希望看到的结果。

还需考虑服务器端的如下情况:“字符串太长,需要分 2 2 2 个数据包发送”。服务器端希望通过调用 1 1 1 次 write 函数传输数据,但如果数据太大,操作系统就有可能把数据分成多个数据包发送到客户端。另外,在此过程中,客户端有可能在尚未收到全部数据包时就调用 read 函数。

所有这些问题都源自 TCP 的数据传输特性。那该如何解决呢?

4.回声客户端问题解决方法

先回顾一下回声服务器端的 I/O 相关代码:

while ((str_len = read(clnt_sock, message, BUF_SIZE)) != 0)
{
    
    
	write(clnt_sock, message, str_len);
}

接着回顾回声客户端的 I/O 相关代码:

write(sock, message, strlen(message));
str_len = read(sock, message, BUF_SIZE - 1);

二者都在循环调用 read 或 write 函数。实际上之前的回声客户端将 100% 接收自己传输的数据,只不过接收数据时的单位有些问题。

回声客户端传输的是字符串,而且是通过调用 write 函数一次性发送的。之后还调用一次 read 函数,期待着接收自己传输的字符串。这就是问题所在。

上述回声客户端问题实际上是初级程序员经常犯的错误,其实很容易解决,因为可以提前确定接收数据的大小。若之前传输了 20 20 20 字节长的字符串,则在接收时循环调用 read 函数读取 20 20 20 个字节即可。既然有了解决方法,接下来给出其代码。

4.1 echo_client2.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling(char *message);

int main(int argc, char *argv[])
{
    
    
	int sock;
	char message[BUF_SIZE];
	int str_len, recv_len, recv_cnt;
	struct sockaddr_in serv_adr;

	if (argc != 3)
	{
    
    
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	
	sock = socket(PF_INET, SOCK_STREAM, 0);

	if (sock == -1)
	{
    
    
		error_handling("socket() error");
	}
	
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family = AF_INET;
	serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
	serv_adr.sin_port = htons(atoi(argv[2]));
	
	// 调用connect函数。若调用该函数引起的连接请求被注册到服务器端等待队列,则connect函数将完成正常调用。
	// 因此,即使通过第41行代码输出了连接提示字符串,如果服务器尚未调用accept函数,也不会真正建立服务关系。
	if (connect(sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1)
		error_handling("connect() error!");
	else
		puts("Connected...........");
	
	while (1)
	{
    
    
		fputs("Input message(Q to quit): ", stdout);
		fgets(message, BUF_SIZE, stdin);
		
		if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) break;

		str_len = write(sock, message, strlen(message));
		
		recv_len = 0;

		while (recv_len < str_len)
		{
    
    
			recv_cnt = read(sock, &message[recv_len], BUF_SIZE - 1);
			if (recv_cnt == -1)
			{
    
    
				error_handling("read() error!");
			}
			recv_len += recv_cnt;
		}
		
		message[recv_len] = 0;

		printf("Message from server: %s", message);
	}
	
	// 调用close函数向相应套接字发送EOF(EOF即意味着中断连接)
	close(sock);

	return 0;
}

void error_handling(char *message)
{
    
    
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

编译运行:

gcc echo_client2.c -o eclient2
./eclient2 127.0.0.1 9190

猜你喜欢

转载自blog.csdn.net/qq_42815188/article/details/129354541