内容概要
第四章通过回声示例讲了一下TCP的服务器/客户端实现方法~ 这只是编程角度,这里通过TCP原理角度出发,讲解这个过程,同时解决掉上一章末尾的问题。
- 回声客户端的修正版
- tcp套接字中的io缓冲原理
- tcp工作原理(握手过程)
正文
5.1 回声客户端的完美实现
5.1.1 只有回声客户端有问题?
这里说的代码参见上篇文章。上篇文章
我截取其中主要说的这部分如下所示,这里是客户端的一部分代码
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));
/*
* fd:显示数据接受对象的文件描述 buf:要保存的数据的缓冲地址值 nbytes:要接收数据的最大字节数
* ssize_t read(int fd, void* buf,size_t nbytes);
* 成功时返回接收的字节数,(但遇到文件结尾则返回0),失败返回-1
*/
str_len = read(sock,message,BUF_SIZE - 1);
message[str_len] = 0;
printf("Message from server: %s", message);
}
这里有一个很重要的问题没有考虑,就是如果客户端在发送数据的后,服务器开始接受,这时如果服务器运行的更慢,可能我们客户端的第二次循环已经开始了!!!又write了一次,那这时我们客户端会read出来个啥呢?
可能是会把所有write进 写入缓冲的数据都 read出来吧~,这肯定不是我们想要的啊!
所以!我们要控制我们客户端的read,让他乖乖的每次只读我们传过去的字节数! nice终于把这个说明白了。。。。
5.1.2 回声客户端解决的办法
实际上解决方法就很简单了,重要的是发现问题的过程,这里其实只要把read放到一个while循环里就ok了~接不到我想要的大小的回声我就一直read,直到我满意为止。
上代码:
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_count;
struct sockaddr_in serv_addr;
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_addr,0,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) == -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(sock);
return 0;
}
void error_handling(char* message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
其中
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;
}
这里要好好理解一下,
recv_cnt 用来存储 每次读到的字节数
read函数将每次读取的信息存到 message中(比如第一次读了3个字节,第二次再存就从数组的第3个位置开始)
可能大家有个问题:while中为什么不用 recv_len != str_len
有可能在接收的时候出现异常,比如 str_len = 5, 第一次接受了3个,第二次又接受了3个,那就没法停止循环了
不能及时发现错误。
5.1.3 如果问题不在于回声客户端:定义应用层协议
上面的程序中,我们在客户端程序中直接定义了write的str_len 这实际上在应用中不太可能直接得到。
这时就需要些应用层的协议了(例如之前的收到 q 就退出)
下面写一个计算器的TCP程序,感受一下应用层协议的定义过程。
要求:先发送几个数字,再发送一种计算方法,返回结果。
细节自己定,我就写个我自己的
operator_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[]) // main的参数不变,还是这样的,一会赋值服务器的IP地址
{
struct sockaddr_in serv_addr; // 服务器地址信息结构体
int sock_client;
int count; // 用来接收输入数字的个数
int result; // 用来接收返回的结算结果
char message[BUF_SIZE]; // 传输用的字符数组
if(argc != 3){
printf("Usage %s <IP> <port>\n", argv[0]);
exit(1);
}
// 1. 分配套接字,并初始化一会连接时用的 服务器端的地址信息
sock_client = socket(PF_INET,SOCK_STREAM,0);
if (sock_client == -1)
{
error_handling("socket() error");
}
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
// 2. 下面进行连接被~
if(connect(sock_client,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) == -1){
error_handling("connected error!");
}
else{
puts("Connected.....");
}
// 3. 连上了开始接受我们的输入数字和符号、同时存入我们想要传过去的字符数组
fputs("请输入想要计算几个数字呀~ : ",stdout);
scanf("%d",&count);
message[0] = (char)count; // 这样写感觉会有问题,如果count是两位数呢,一个位置是不就装不下了
for(int i = 0;i < count ;i++){
printf(" 请输入第 %d 个数:",i+1);
scanf("%d",(int*)&message[4*i+1]);
}
fgetc(stdin); // 删除缓冲中的字符\n
fputs("请输入操作符:",stdout);
scanf("%c",&message[count*4 + 1]);
// 4. 该输入的搞定了,接下来就传过去被~ 传完了接回来结果。
write(sock_client,message,count *4 + 2);
printf("客户端已经运行write函数\n");
read(sock_client,&result,4); // 因为是 int 所以最大4个字节
printf("客户端已经运行read函数\n");
printf("计算结果为:%d:",result);
close(sock_client);
return 0;
}
void error_handling(char* message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
下面是服务器端代码
operator_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 calculate(int num_count,int recv_message[],char operator);
int main(int argc, char* argv[]) // main的参数不变,还是这样的,一会赋值服务器的IP地址
{
struct sockaddr_in server_addr,client_addr;
socklen_t client_addr_len;
int server_sock,client_sock;
int result = 0,num_count = 0;
int recv_len,temp_recv;
char recv_message[BUF_SIZE];
if(argc != 2){ // 除了程序名还有1个参数
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
//声明变量不说了,一会在上面用一个声明一个
// 1. 创建服务器端的套接字 并初始化地址结构体其中的地址信息
server_sock = socket(PF_INET,SOCK_STREAM,0);
if(server_sock == -1){
error_handling("socket() error!");
}
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(atoi(argv[1]));
// 2. 用bind给创建好的server_sock分配一下地址信息
if(bind(server_sock,(struct sockaddr*)&server_addr,sizeof(server_addr)) == -1){
error_handling("bind() error");
}
// 3. 三步走最后一步 listen
if(listen(server_sock,5) == -1){
error_handling("listen() error ");
}
printf("init ok\n");
// 4. 开始正式接收啦~ 一共有5个机会哦
for(int i = 0; i < 5; i++){
client_sock = accept(server_sock,(struct sockaddr*)&client_addr,&client_addr_len);
read(client_sock,&num_count,1); // 接收 一个字符的 计算的个数
// 接下来接收 计算的数字(为了防止缓冲区爆炸,我们用while循环来接收我们想要的字节数)
recv_len = 0;
while(recv_len < (num_count*4 + 1)) // 4*数字个数+1个计算字符
{
temp_recv = read(client_sock,&recv_message[recv_len],BUF_SIZE -1);
recv_len += temp_recv;
}
// 读取结束,开始计算
result = calculate(num_count,(int*)recv_message,recv_message[recv_len-1]);
// 计算结束,传送结果给客户端
write(client_sock,(char*)&result,sizeof(result));
close(client_sock);
}
close(server_sock);
return 0;
}
int calculate(int num_count,int recv_message[],char operator)
{
int result = 0;
switch(operator)
{
case '+':
for(int i = 0; i < num_count;i++){
result += recv_message[i];
}
break;
case '-':
for(int i = 0; i < num_count;i++){
result -= recv_message[i];
}
break;
case '*':
for(int i = 0; i < num_count;i++){
result *= recv_message[i];
}
break;
}
return result;
}
void error_handling(char* message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
TCP原理
这部分讲解之前没有讲的一些细节,补充TCP的原理,为后面理解套接字选项打基础。
5.2.1 TCP套接字中的I/O缓冲
TCP套接字的数据收发无边界,也就是说,服务器调用一次write函数传输了 2个字节,客户端可以分两次调用read函数,每次读一个,也可以一次把两个都读了。
如果每次只读部分数据,那剩下的数据去哪里了呢?
实际上,write函数调用之后并非立即传输数据,read函数调用之后也并非马上接受数据。实际情况如下图所示
调用write函数时,数据将移到输出缓冲中,在适当时候(不管是分别传送还是一次性传送)传向对方的输入缓冲。
这时对方将调用read函数从输入缓冲读取数据。
I/O缓冲特性如下
1. I/O缓冲在每个TCP套接字中单独存在。
2. I/O缓冲在创建套接字时自动生成。
3. 即使关闭套接字 也会继续传输 输出缓冲中遗留的数据。(能够继续往外传)
4. 关闭套接字将丢失输入缓冲中的数据。(不能继续接收数据)
这里提出一个问题?
“客户端当前的空闲输入缓冲为50字节,而服务器传输了100字节怎么办?”
其实这是一个伪命题,在TCP中这是不会出现的,因为TCP会控制数据流。通过TCP中的滑动窗口协议,其内容类似下面的对话内容:
套接字A;“你好,最多可以向我传输50字节的内容”
套接字A;“好的”
套接字A;“您好,我腾出了20个字节的空间,现在最多可以接收70个字节”
套接字A;“好的”
既然write函数会把数据传输到输出缓存中去,那write函数执行结束返回时代表着什么呢?
其实,write 和 win下的 send函数不会在完成向对方主机的数据传输时返回,而是在将数据移到输出缓冲时返回。
TCP会保证输出缓冲数据的传输。
5.2.2 TCP内部工作原理1:与对方套接字的连接(包含传说中的三次握手)
TCP套接字从创建到消失的过程分为下面3步:
- 与对方套接字建立连接
- 与对方套接字进行数据交换
- 断开与对方套接字的连接
首先讲解第一步:与对方套接字建立连接(三次握手)
其过程如下:
shake1 : 套接字A:您好,套接字B。我有数据要传,建立连接。
shake2 :好的我这边已就绪。
shake3:收到!谢谢受理。
TCP在实际通信过程中也会经历3次对话过程,又称 Three-way handshaking(三次握手)。
其连接过程中的实际信息格式如下图所示:
套接字是以双全工(Full-duplex)方式工作的,可以双向传递数据。我们解释一下上面图中的内容
首先说明下:
SYN,ACK是标志位。
SEQ,AN是数据包序号。
【SYN】SEQ:1000,ACK:- (对应shake1)
上方含义:“当前传递数据包序号为1000,如果接受无误,请通知我发送1001号数据包”
【SYN+ACK】SEQ:2000,ACK:1001 (对应shake2)
上方含义:“当前传递数据包序号2000,如果接受无误,请通知我发送2001号数据包;
你发送的1000号数据包已收到,请传递SEQ为1001的数据包”
【ACK】SEQ:1001,ACK:2001 (对应shake3)
上方含义:“当前传递数据包序号1001,如果接受无误,请通知我发送1002号数据包
你发送的2000号数据包已经接受,请传递2001号数据包”
5.2.3 TCP内部工作原理2:与对方主机的数据交换(第二步)
上面通过第一步 的 三次握手完成了数据交换的准备,下面开始收发数据。
下图为:TCP套接字的数据交换过程
上图为 主机A分2次(分两个数据包)向主机B传递200字节的过程。
首先主机A通过1个数据包发送100个字节的数据,数据包的SEQ为1200.
主机B为了确认这一点,向主机A发送ACK1301消息。
我们来解释一下上面的过程:
SEQ:1200
上方含义:“当前传递数据包序号为1200,里面包含100 byte data ,如果接受无误,请通知我发送1301号数据包”
ACK:1301
上方含义:“ 你发送的数据包已收到,请传递SEQ为1301的数据包”
SEQ:1301
上方含义:“当前传递数据包序号1301,里面包含100字节的数据,如果接受无误,请通知我发送1402号数据包”
ACK:1402
上方含义:“你发送数据包已收到,请传递SEQ为1402的数据包”
从上面的过程可看出 序号并不是连贯的,而是增加了字节数大小的序号。
即 ACK号 = SEQ号 + 传递的字节数 + 1
上方是传递正常时的情况。下面看一下如果传递过程中数据包消失的情况。
图中通过SEQ 1301数据包向主机B传递100字节数据,但是中间发生了错误。主机B未收到。
超过一段时间后,主机A仍为接收到SEQ 1301 的ACK确认,因此试着重传该数据包。
为了完成数据包重传,TCP套接字启动计数器以等待ACK应答,若相应计时器发生超市(Time-out)则重传。
5.2.4 断开与套接字的连接(第三步:四次握手)
断开连接时需要双方协商,其对话如下:
套接字A:我希望断开连接~
套接字B:好的,请稍后
套接字B:我已经准备就绪,可以断开
套接字A:好的,收到。
FIN表示断开连接。也就是说双方各发一次FIN消息后断开连接,此过程有4个阶段又称4次握手。
【FIN】SEQ:5000,ACK:- (对应shake1)
上方含义:“当前传递数据包序号为5000,我请求断开连接!如果接受无误,请通知我发送5001号数据包”
【ACK】SEQ:7500,ACK:5001 (对应shake2)
上方含义:“当前传递数据包序号7500,如果接受无误,请通知我发送7501号数据包;
你发送的断开请求已收到,请稍等,下次请传递SEQ为5001的数据包”
【FIN】SEQ:7501,ACK:5001 (对应shake3)
上方含义:“当前传递数据包序号为7501,如果接受无误,请通知我发送7502号数据包
ok 我已经准备好了,下次可以传递SEQ为 5001 的数据包”
【ACK】SEQ:5001,ACK:7502 (对应shake4)
上方含义:“当前传递数据包序号5001,如果接受无误,请通知我发送5002号数据包
你发送的7501号数据包已经接受”
有个问题:为什么B给A发了两次一帮的东西,其实,第二次FIN数据包中的ACK 5001只是因为接受ACK消息后未接受数据重传的。