Part 1 start network programming:chapter FIVE: 基于TCP的服务器端/客户端(2)

内容概要

第四章通过回声示例讲了一下TCP的服务器/客户端实现方法~ 这只是编程角度,这里通过TCP原理角度出发,讲解这个过程,同时解决掉上一章末尾的问题。

  1. 回声客户端的修正版
  2. tcp套接字中的io缓冲原理
  3. 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步:

  1. 与对方套接字建立连接
  2. 与对方套接字进行数据交换
  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消息后未接受数据重传的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值