此篇博客编写回声(echo)服务器端/客户端。顾名思义,服务器端将客户端传输的字符串数据原封不动地传回客户端,就像回声一样。在此之前,需要先解释一下迭代服务器端。

实现迭代服务器端

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

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

调用 close 函数就意味着结束了针对某一客户端的服务。此时如果还想服务于其他客户端,就要重新调用 accept 函数。

目前,我们实现的服务器,同一时刻只能服务于一个客户端。将来学完进程和线程后,就可以编写同时服务多个客户端的服务器端了。

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

前面讲的就是迭代服务器端。即使服务器端以迭代方式运转,客户端代码亦无太大区别。接下来创建迭代回声服务器端及其配套的回声客户端。首先整理一下程序的基本运行方式。

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

回声服务器端代码

首先介绍满足以上要求的回声服务器端代码。希望大家注意观察 accept 函数的循环调用过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#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, clnt_adr;
socklen_t clnt_adr_size;

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_size = sizeof(clnt_adr);

for (i = 0; i < 5; ++i)
{
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_size);
if (clnt_sock == -1)
{
error_handling("accept() error!");
}
else
{
printf("Connected client %d.\n", i);
}

while ((str_len = read(clnt_sock, &message, sizeof(message))) != 0)
{
if (str_len == -1)
{
error_handling("read() error!");
}
write(clnt_sock, message, str_len);
}
close(clnt_sock);
}
close(serv_sock);

return 0;
}

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

回声客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.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);

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]));

if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
{
error_handling("conect() error!");
exit(1);
}
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);
if (str_len == -1)
{
error_handling("read() error!");
}
message[str_len] = '\0';
printf("Message from server: %s\n", message);
}

close(sock);

return 0;
}

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

改进回声客户端

回声客户端存在的问题

下列是上面回声客户端代码的第 53 ~ 61 行。

1
2
3
4
5
6
7
8
9
write(sock, message, strlen(message));

str_len = read(sock, message, BUF_SIZE - 1);
if (str_len == -1)
{
error_handling("read() error!");
}
message[str_len] = '\0';
printf("Message from server: %s\n", message);

以上代码有个错误假设:

  • 每次调用 read、write 函数时都会以字符串为单位执行实际的 I/O 操作。

当然,每次调用 write 函数都会传递 1 个字符串,因此这种假设在某种程度上也算合理,但大家还记得“TCP 不存在数据边界”吗?上述客户端是基于 TCP 的,因此,多次调用 write 函数传递的字符串有可能一次性传递到服务器端。此时客户端有可能从服务器端收到多个字符串,这不是我们希望看到的结果。

还需考虑服务器端的如下情况:

  • 字符串太长,需要分 2 个数据包发送!

服务器端希望通过 1 次 write 函数传输数据,但如果数据太大,操作系统就可能把数据分成多个数据报发送到客户端。另外,在此过程中,客户端有可能在尚未收到全部数据包时就调用 read 函数。

  • 但是上述示例不是正常运转了吗?

当然,我们的回声服务器/客户端给出的结果是正确的。但这只是运气好罢了!只是因为收发的数据小,而且运行环境为同一台计算机或相邻的两台计算机,所以没发生错误,可实际上仍存在发生错误的可能。

  • 回声服务器端没有问题,只有回声客户端有问题?

问题不在服务端,而在客户端。但只看代码或许不太好理解,因为 I/O 中使用了相同的函数。先回顾一下回声服务器端的 I/O 相关代码,下面是回声服务器代码的第 62 ~ 69行。

1
2
3
4
5
6
7
8
while ((str_len = read(clnt_sock, &message, sizeof(message))) != 0)
{
if (str_len == -1)
{
error_handling("read() error!");
}
write(clnt_sock, message, str_len);
}

接着回顾客户端代码,下面是回声客户端代码的 53 ~ 55 行。

1
2
3
write(sock, message, strlen(message));

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

二者都在循环调用 read 或 write 函数。实际上之前的回声客户端将 100% 接收自己传输的数据,只不过接收数据事的单位有些问题。扩展客户端代码回顾范围,下面是客户端代码的第 43 ~ 62 行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
if (str_len == -1)
{
error_handling("read() error!");
}
message[str_len] = '\0';
printf("Message from server: %s\n", message);
}

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

  • 既然回声客户端会受到所有字符串数据,是否只需多等一会儿?过一段时间后再调用 read 函数是否可以一次性读取所有字符串数据呢?

的确,过一段时间后即可接收,但需要等多久?要等 10 分钟吗?这不符合常理,理想的客户端应在收到字符串数据时立即读取并输出。

回声客户端问题解决方法

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

  • 改进版回声客户端代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.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;
int recv_len, recv_cnt;

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

sock = socket(PF_INET, SOCK_STREAM, 0);

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]));

if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
{
error_handling("conect() error!");
exit(1);
}
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);
// if (str_len == -1)
// {
// error_handling("read() error!");
// }

str_len = strlen(message);

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


message[str_len] = '\0';
printf("Message from server: %s\n", message);
}

close(sock);

return 0;
}

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

如果问题不在于回声客户端

如果问题不在于回声客户端:定义应用层协议。

回声客户端可以提前知道接收的长度,但我们应该意识到,更多情况下这不太可能。既然如此,若无法预知接收数据长度时应如何收发数据?此时需要的就是应用层协议的定义。之前的回声服务器端/客户端中定义了如下协议。

  • 收到 Q 就立即终止连接。

同样,收发数据过程中也需要定好规则(协议)以表示数据的边界,或提前告知收发数据的大小。服务器端/客户端实现过程中逐步定义的这些规则集合就是应用层协议。

体验应用层协议的定义过程,查看基于 TCP/IP 的服务器/客户端实现数值运算