云计算百科
云计算领域专业知识百科平台

Linux 网络编程(三)——基于TCP的服务器端/客户端

文章目录

3 基于TCP的服务器端/客户端

3.1 TCP 服务器端的默认函数调用顺序

3.2 TCP客户端的默认函数调用顺序

3.3 基于TCP的服务器端/客户端函数调用关系

3.4 实现迭代服务器端/客户端

3.5 回声客户端完美实现

3.5.1 回声客户端问题解决方法

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


3 基于TCP的服务器端/客户端

3.1 TCP 服务器端的默认函数调用顺序

3.2 TCP客户端的默认函数调用顺序

我们观察上一篇文章中最后的 hello_client.c 代码发现,实现服务器端必须经过的步骤之一就是为套接字分配 IP 地址和端口号,但在客户端实现过程中,并未显式进行套接字地址分配,而是在创建套接字后直接调用了 connect 函数。这是否意味着客户端套接字无需分配 IP 和端口号呢?当然不是,因为客户端在调用 connect 函数时,操作系统内核会自动为其分配 IP 地址和端口号,而无需显式调用 bind 函数进行分配。

3.3 基于TCP的服务器端/客户端函数调用关系

服务器端创建套接字后连续调用 bind、listen 函数进人等待状态,客户端通过调用 connect 函数发起连接请求。需要注意的是,客户端只能等到服务器端调用 listen 函数后才能调 connect 函数同时要清楚,客户端调用 connect 函数前,服务器端有可能率先调用 accept 函数。当然,此时服务器端在调用 accept 函数时进人阻塞(blocking)状态,直到客户端调 connect 函数为止。

客户端调用 connect 函数后,发生如下情况之一才会返回(完成函数调用)

  • 服务器端接收连接请求
  • 发生断网等异常情况而中断连接请求

需要注意,所谓的 “接收连接” 并不意味着服务器端调用 accept 函数,其实是服务器端把连接请求信息记录到等待队列。因此connect函数返回后并不立即进行数据交换。

3.4 实现迭代服务器端/客户端

回声服务端/客户端就是将客户端传输的字符串数据原封不动地传回客户端,就像回声一样。前一章节的 Hello world 服务器端处理完 1 个客户端连接请求即退出,连接请求等待队列实际没有太大意义,也并不符合实际需求,因此需要插入循环语句反复调用 accept 函数。

需要注意的是,在调用 close 函数时,关闭的文件描述符是 accept 函数调用时创建的套接字(用来通信的套接字)。调用 close 函数就意味着结束了针对某一客户端的服务。此时如果还想服务于其他客户端,就要重新调用accept函数。目前同一时刻只能服务于一个客户端,将来可以通过多线程编写同时服务多个客户端的服务器端。

(1)案例需求

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

(2)服务端 echo_server.c

#define handle_error(cmd,result) \\
if(result < 0) \\
{ \\
perror(cmd); \\
return -1; \\
} \\

int main(int argc, char const *argv[])
{
struct sockaddr_in serv_addr,clnt_addr;
char* message = NULL;
message = malloc(sizeof(char)*1024);
memset(&serv_addr,0,sizeof(serv_addr));
memset(&clnt_addr,0,sizeof(clnt_addr));
if(argc != 2) {
printf("Usage:%s <port>\\n",argv[0]);
exit(1);
}
if (!message) {
perror("malloc server message");
return 1;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(atoi(argv[1]));
inet_pton(AF_INET,"0.0.0.0",&serv_addr.sin_addr);

int serv_sock = socket(AF_INET,SOCK_STREAM,0);
handle_error("socket",serv_sock);

int temp = bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
handle_error("bind",temp);

temp = listen(serv_sock,128);
handle_error("listen",temp);

int count;
socklen_t clnt_len = sizeof(clnt_addr);
for(int i = 0;i < 5;i++) {
int clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,&clnt_len);
if(clnt_sock == -1) {
handle_error("accept",clnt_sock);
}
else {
printf("Connected client %d\\n",i+1);
}
//接收数据,存储在缓存message
while((count = recv(clnt_sock,message,1024,0)) != 0) {
printf("Message from client%d:%s",i+1,message);
//将接收到的数据发送回去
temp_result = send(clnt_sock,message,1024,0);
}
close(clnt_sock);
}
close(serv_sock);
free(message);
return 0;
}

(3)客户端 echo_client.c

#define handle_error(cmd,result) \\
if(result < 0) \\
{ \\
perror(cmd); \\
return -1; \\
} \\

int main(int argc, char const *argv[])
{
struct sockaddr_in serv_addr,clnt_addr;
char* message = NULL;
message = malloc(sizeof(char)*1024);
memset(&serv_addr,0,sizeof(serv_addr));
memset(&clnt_addr,0,sizeof(clnt_addr));
if(argc != 3) {
printf("Usage:%s <IP> <port>\\n",argv[0]);
exit(1);
}
if (!message) {
perror("malloc server message");
return 1;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET,argv[1],&serv_addr.sin_addr);

int clnt_sock = socket(AF_INET,SOCK_STREAM,0);
handle_error("socket",clnt_sock);

int temp=connect(clnt_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
handle_error("connect",temp);
printf("Connected……\\n");

while(1) {
fputs("Input message(Q to quit):",stdout);
//从控制台获取数据
fgets(message,1024,stdin);
if(!strcmp(message,"q\\n") || !strcmp(message,"Q\\n"))
break;
temp_result = send(clnt_sock,message,1024,0);
//接收服务端发送过来的数据
temp_result = recv(clnt_sock,message,1024,0);
printf("Message from server:%s",message);
}
close(clnt_sock);
free(message);
return 0;
}

(4)测试结果

  • 下图是第一张测试结果图。我们发现,通过这种 for 循环的方式,服务端同一时刻只能与一个客户端相连,服务器端把连接请求信息记录到等待队列,因此 connect 函数后并不会立即返回数据,而是处于挂起状态

  • 下图是第二张测试结果图。当客户端1关闭连接后,客户端 2 得以与服务器端进行通信,而客户端 3 仍然被挂起在请求队列中。无论客户端3发送何种数据,服务端均无法接收。

(5)回声客户端存在的问题

  • 客户端是基于 TCP 的,多次调用 send 函数传递的字符串有可能一次性传递到服务器端。此时客户端有可能从服务器端收到多个字符串,我们希望的是一次接收一个。(客户端发送速率远大于服务端接收速率)
  • 服务器端希望通过调用1次 send 函数传输数据,但如果客户端发送过来的数据太大,操作系统就有可能把数据分成多个数据包发送到客户端。在此过程中,客户端有可能在尚未收到全部数据包时就调用 recv 函数。

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

3.5 回声客户端完美实现

3.5.1 回声客户端问题解决方法

这个问题其实很容易解决,因为可以提前设定接受数据的大小。若之前传输了20字节长的字符串,则再接收时循环调用 recv 函数读取 20 个字节即可。既然有了解决办法,修改 3.4 小节客户端 echo_client.c 代码中 while 循环代码。

while(1) {
fputs("Input message(Q to quit):",stdout);
//从控制台获取数据
fgets(message,1024,stdin);
if(!strcmp(message,"q\\n") || !strcmp(message,"Q\\n"))
break;

//发送了多少字节数据,就循环接收接收多少字节数据
str_len = send(clnt_sock,message,1024,0);
recv_len = 0;
//若写成 recv_len=str_len 可能会出现无限循环
while (recv_len < str_len) {
recv_cnt = recv(clnt_sock,message,1024,0);
recv_len += recv_cnt;
}
printf("Message from server:%s",message);
memset(message,0,1024);
}

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

回声客户端可以提前知道接收数据的长度,这在大多数情况下是不可能的。那么此时无法预知接收数据长度时应该如何收发数据?这时需要的是应用层协议的定义。在收发过程中定好规则(协议)以表示数据边界,或者提前告知需要发送的数据的大小。服务端/客户端实现过程中逐步定义的规则集合就是应用层协议。现在写一个小程序来体验应用层协议的定义过程。要求:

  • 服务器从客户端获得多个数组和运算符信息。
  • 服务器接收到数字候对齐进行加减乘运算,然后把结果传回客户端。
  • 向服务器传递 3,5,9 的同事请求加法运算,服务器返回 3+5+9 的结果
  • 请求做乘法运算,客户端会收到 3*5*9 的结果
  •  如果向服务器传递 4,3,2 的同时要求做减法,则返回 4-3-2 的运算结果。

(1)服务器端 op_server.c 实现

int calculate(int optnum, int opts[],char op);
int main(int argc, char const *argv[])
{
struct sockaddr_in serv_addr, clnt_addr;
char* read_buf = malloc(sizeof(char)*1024);
memset(&serv_addr,0,sizeof(serv_addr));
memset(&clnt_addr,0,sizeof(clnt_addr));
if(argc != 2) {
printf("Usage:%s <port>\\n",argv[0]);
exit(1);
}
serv_addr.sin_family = AF_INET;
inet_pton(AF_INET,"0.0.0.0",&serv_addr.sin_addr);
serv_addr.sin_port = htons(atoi(argv[1]));

int serv_sock = socket(AF_INET,SOCK_STREAM,0);
handle_error("socket",serv_sock);

int temp=bind(serv_sock,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
handle_error("bind",temp);

temp= listen(serv_sock,128);
handle_error("listen",temp);

socklen_t serv_len = sizeof(serv_addr);
int recv_cnt;
for(int i = 0;i < 5;i++) {
int opt_count = 0;
int clnt_sock=accept(serv_sock,(struct sockaddr*)&serv_addr,&serv_len);
handle_error("accept",clnt_sock);
printf("与客户端%s %d相连,文件描述符是%d\\n",
inet_ntoa(clnt_addr.sin_addr),ntohs(clnt_addr.sin_port),clnt_sock);
recv(clnt_sock,&opt_count,1,0);
int recv_len = 0;
//因为read_buf是char*,int类型会占用4个字节位置 + char类型1个字节
while(recv_len < opt_count * 4 + 1) {
recv_cnt = recv(clnt_sock,&read_buf[recv_len],1024,0);
recv_len += recv_cnt;
}
int result = calculate(opt_count,(int *)read_buf,read_buf[recv_len-1]);
printf("Operation result:%d\\n",result);
send(clnt_sock,&result,sizeof(result),0);
close(clnt_sock);
}
close(serv_sock);
free(read_buf);
return 0;
}

int calculate(int optnum, int opts[],char op) {
int result = opts[0], i;
switch (op)
{
case '+':
for (i = 1; i < optnum; i++)
result += opts[i];
break;
case '-':
for (i = 1; i < optnum; i++)
result -= opts[i];
break;
case '*':
for (i = 1; i < optnum; i++)
result *= opts[i];
break;
}
return result;
}

(2)客户端 op_client.c 实现

#define handle_error(cmd,result) \\
if(result < 0) \\
{ \\
perror(cmd); \\
return -1; \\
} \\

int main(int argc, char const *argv[])
{
int str_len,recv_len,recv_cnt;
struct sockaddr_in serv_addr,clnt_addr;
char* write_buf = malloc(sizeof(char)*1024);
memset(&serv_addr,0,sizeof(serv_addr));
memset(&clnt_addr,0,sizeof(clnt_addr));
if(argc != 3) {
printf("Usage:%s <IP> <port>\\n",argv[0]);
exit(1);
}
if (!write_buf) {
perror("malloc server write_buf");
return 1;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET,argv[1],&serv_addr.sin_addr);

int clnt_sock = socket(AF_INET,SOCK_STREAM,0);
handle_error("socket",clnt_sock);

int temp_result = connect(clnt_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
handle_error("connect",temp_result);
printf("Connected……\\n");

int opt_count; //操作数的数量
fputs("Operand count:",stdout);
scanf("%d",&opt_count);
write_buf[0] = (char)opt_count; //将int数据保存在char数组
//输入操作数
for(int i = 0;i< opt_count;i++) {
printf("Operand%d:",i+1);
scanf("%d",(int *)&write_buf[i*4+1]); //4字节int类型保存到char数组中
}
//删掉缓冲中\\n
fgetc(stdin);
//输入操作符
fputs("Operator:",stdout);
scanf("%c",&write_buf[opt_count*4+1]);
send(clnt_sock,write_buf,opt_count*4+2,0);
int result;
recv(clnt_sock,&result,sizeof(int),0);
printf("Operation result:%d\\n",result);
close(clnt_sock);
free(write_buf);
return 0;
}

从图中可以看出,若想在同一数组中保存并传输多种数据类型,应把数组声明为char类型。而且需要额外做一些指针及数组运算。

(3)测试结果

赞(0)
未经允许不得转载:网硕互联帮助中心 » Linux 网络编程(三)——基于TCP的服务器端/客户端
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!