这一章是在上一章的基础上写的,如果没看过,请移步基于TCP的服务器端、客户端(上),通过这一章你会更加理解为什么网络编程里面需要许多协议,以及明白各种协议的使用是为了解决各种特殊的问题,回声测试就是一个很好的说明例子。 注意本文所有代码测试环境是在Linux系统下,而非windows
一、回声测试问题解决
1.1 检查问题
我们在上一章分析过问题,所以这里不再赘述;但我们需要知道的是服务端是没有问题问题出在客户端。
下面代码是上一章客户端与服务端部分代码
while((str_len=read(clnt_sock,message,BUF_SIZE))!=0)
write(clnt_sock,message,str_len);
while(1)
{
fputs("Input message(Q to quit):",stdout);
fgets(message,BUF_SIZE,stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
{
printf("testing......\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函数实现,注意观察客户端,通过write一次性发送一串字符串,并期望接收自己刚刚发出去的字符串并通过调用一次read()将字符串读出来, 因此客户端是接收数据的单位是串,而非字符数,问题就出在这,同时这里预设了一个前提:客户端知道自己会接收怎样的字符串。但是在实际场合中,客户端并不知道自己会接收到怎样的字符串,也许是一串,也许是多串。但是假如收到多串字符串,客户端是否应该等到所有字符串接收完毕再进行读取呢?但是客户端怎么知道等多久?因此比较现实点的做法就是:数据来了我就读取并输出。由于我们暂时没有介绍多进程概念(我会在后面进行补充,有需要可以关注我!),所以用其他方法代替实现。
1.2 客户端解决方法
将接收数据的单位由串改为字节,由于数据发出去的时候已经能算出将要接收数据的字节,因此若之前传输了20字节长的字符串,则在接收时循环调用read函数读取20个字节即可,以下是客户端修改之后的代码(服务端代码不变),你可能在想实际上客户端并不知道要接收到多少数据,注意这是一个过渡的解决方案。
#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;
int 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]));
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"))
{
printf("testing......\n");
break;
}
str_len=write(sock,message,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[recv_len]=0;
printf("Message from server:%s",message);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
测试结果:
修改前:客户端的read()函数只调用了一次,不能保证将所有数据接收完毕
修改后:客户端的read()函数可能调用了多次保证了数据能够接收完毕,同时满足“TCP不存在数据边界”特点
1.3 高阶解决方法
上面的解决方案还有一个漏洞,正如我在上一篇博客里面讲的,客户端实际上是无法知道自己要接收到多少数据的,若无法预知接收数据长度时应如何收发数据?此时需要的就是应用层协议的定义,通过定义协议,来确定数据的边界,或者提前告知数据的大小。(服务端、客户端实现过程中逐步定义的这些规则集合就是应用层协议)我们下面通过一个小实验体验一下应用层协议定义过程。
实验过程
服务端从客户端获得多个数字和运算符信息,服务器端收到数字后对其进行加减乘运算,然后把结果返回到客户端并显示。
1.4 计算机服务端、客户端实例
-
头文件 #include<stdio.h>
#include <stdlib.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<unistd.h>
#include<string.h>
#define BUF_SIZE 1024
#define RLT_SIZE 4
#define OPSZ 4
void error_handling(char *message);
-
客户端实现 #include "internet.h"
int main(int argc,char* argv[])
{
int sock;
char opmsg[BUF_SIZE];
int result,opnd_cnt,i;
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]));
if(connect(sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr))==-1) error_handling("connect() error");
else{
puts("connected....");
}
fputs("Operand count:",stdout);
scanf("%d",&opnd_cnt);
opmsg[0]=(char)opnd_cnt;
for(int i=0;i<opnd_cnt;i++)
{
printf("Operand %d:",i+1);
scanf("%d",(int *)&opmsg[i*OPSZ+1]);
}
fgetc(stdin);
fputs("OPerator: ",stdout);
scanf("%c",&opmsg[opnd_cnt*OPSZ+1]);
write(sock,opmsg,opnd_cnt*OPSZ+2);
read(sock,&result,RLT_SIZE);
printf("Operation result:%d\n",result);
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
我们将操作数、运算符、放在同一个数组opmsg里面,方便数据的传输,该数组的结构如下所示,这也就是客户端代码第24-28行将opmsg强制类型转换的原因:若想在同一数组当中保存并传输多种类型数据,应该把数组声明为char类型。
-
服务端实现 #include"internet.h"
int calculate(int opnum, int opnds[],char oprator);
int main(int argc,char *argv[])
{
int serv_sock,clnt_sock;
char opinfo[BUF_SIZE];
int result,opnd_cnt,i;
int recv_cnt,recv_len;
struct sockaddr_in serv_adr,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);
for(i=0;i<5;i++)
{
opnd_cnt=0;
clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_adr,&clnt_adr_sz);
read(clnt_sock,&opnd_cnt,1);
recv_len=0;
while((opnd_cnt*OPSZ+1)>recv_len)
{
recv_cnt=read(clnt_sock,&opinfo[recv_len],BUF_SIZE-1);
recv_len+=recv_cnt;
}
result=calculate(opnd_cnt,(int *)opinfo,opinfo[recv_len-1]);
write(clnt_sock,(char *)&result,sizeof(result));
close(clnt_sock);
}
close(serv_sock);
return 0;
}
int calculate(int opnum,int opnds[],char oprator)
{
int result=opnds[0],i;
switch (oprator)
{
case '+':
for(i=1;i<opnum;i++)result+=opnds[i];
break;
case '-':
for(i=1;i<opnum;i++)result-=opnds[i];
break;
case '*':
for(i=1;i<opnum;i++)result*=opnds[i];
break;
}
return result;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
-
实验结果
先启动服务端:./server 9091
再启动客户端 ./client 127.0.0.1 9091
connected....
Operand count:3
Operand 1:12
Operand 2:24
Operand 3:36
OPerator: +
Operation result:72
到这算是成功了。
二、TCP原理
本节目的在于为日后理解套接字(第九章)的基础,可以选择性地看。
2.1 TCP套接字中的I/O缓冲
在之前,或多或少提过TCP套接字的数据收发不存在边界,服务器即使调用一次write函数传输40字节的数据,客户端可能得调用4次read函数每次读取10字节接收数据;但客户端端第一次接收10字节之后,剩下的30字节在何处等待客户端继续调用read函数进行接收呢?难道这30字节在网络数据流当中盘旋等待客户端接收?
实际上,write函数调用之后并非立即将数据传输出去,而是写入缓冲区当中,同样的,read函数并非从网络中读取数据,而是从缓冲区里面读取数据。如下图所示:
write函数将数据写入缓冲区,在适当的时候传到对方的输入缓冲当中。反之亦然
I/O缓冲特性可以总结如下:
- I/0缓冲在每个TCP套接字当中独立存在。
- I/O缓冲在创建套接字时自动生成,不需要手动创建。
- 即使关闭套接字也会继续传递输出缓冲中遗留的数据。
- 关闭套接字将丢失输入缓冲中的数据
那么发送的数据大于输入缓冲区大小,会不会导致数据丢失呢?
答:不会,同样,要解决这个问题,需要制定一套网络协议进行协调;我们比较熟悉的协议例如:TCP中的滑动窗口协议,进行控制数据流。以下对话过程可以大概理解以下协议流程:
套接字A:“您好!,最多可以向我传递50字节的数据”
套接字B:“好的!”
套接字A:“我腾出了20字节的空间,最多可以接收20字节的数据”
套接字B:“好的!”
2.2 TCP原理1(与对方套接字的连接)
TCP从创建到消失经过以下过程:
-
建立连接 建立连接的交互
套接字A:“您好!套接字B。我这儿有数据传给你,需要和你建立连接”(发送连接请求)
套接字B:“好的,我这边已经准备好了”(accept 连接)
套接字A:“谢谢@!”(回复)
而在实际的建立连接过程当中是通过三次握手完成的。
套接字是以全双工方式工作的即数据可以双向传递,三次握手流程如下(ACK是确认消息,SEQ是同步消息)
[SYN] : SEQ =1000 ACK= – //SEQ=1000意思是现在传递的数据包序号为1000,如果接收无误,请通 知我向您传递1001号数据包。
[SYN+ACK]:SEQ=2000 ACK=1001 //SEQ=2000表示 现在接收的数据包为2000,如果接收无误,请通知我向你传递传递2001号数据包; ACK=1001 表示 刚刚序号为SEQ=1000数据包接收无误,现在请传递SEQ为1001的数据包。(注意,主机B给主机A发送的确认消息也算数据包)
[ACK] : SEQ=1001 ACK=2001 已正确收到传输的SEQ为2000的数据包,现在可以传输SEQ为2001的数据包
自此,主机A,B确认了彼此均就绪。
-
数据传输 -
断开连接
2.3 TCP原理2(与对方主机的数据交换)
-
建立连接 … -
数据传输 经过了第一步的三次握手,开始进行数据传输。 这里展示主机A分两次向B发送200byte数据的过程。
首先主机A向B发送100byte数据,数据包SEQ为1200,而B为了向A确认收到数据便向主机A发送ACK1301(而不是1201,原因在于ACK号的增加的量为传输的数据字节数,假设每次ACK不加传输的字节数,这样虽然可以确认数据包的传输,但无法明确100字节全部正确传递还是丢失了一部分)ACK计算方法:ACK=SEQ号+传递的字节数+1(加一的目的在于告诉对方下次传递的SEQ号)。
如果出现包的丢失,主机会有一个计时器,如果在固定时间内没有收到发送出去的包的回馈信息,发送方会重新发送包。
- 断开连接
2.4 TCP 原理3(断开与套接字的连接)
-
建立连接 … -
数据传输 … -
断开连接 先由套接字A向B发送断开连接的信息,套接字B发出确认收到的信息,然后向套接字A传递可以断开的信息,套接字A同样发出确认信息,流程图如下图所示。
FIN表示断开连接,也就是说双方各发送一次FIN消息之后断开连接。此过程经历四个阶段,又称为四次握手。SEQ与ACK的含义与作用与之前的一样,但是你会发现图中主机B给 主机A传了两次ACK=5001,这是因为接收ACK消息之后未接收数据而重传的。
|