TCP IP网络编程(五) 基于TCP的服务器端、客户端 (补充)

文章目录

回声客户端的完美实现

回声客户端出现的问题

在上一节基于TCP的服务器端、回声客户端中,存在问题:

如果数据太大,操作系统就有可能把数据分成多个数据包发送到客户端,客户端有可能在尚未收到全部数据包时就调用read函数

问题出在客户端,而不是服务器端,先来对比一下客户端与服务器端I/O相关代码

服务器端

c++ 复制代码
while((str_len = read(cknt_sock, message, BUF_SIZE)) != 0)
    write(clnt_sock, message, str_len);

客户端

c+ 复制代码
write(sock, message, strlen(message))
str_len = read(sock, message, BUF_SIZE - 1);

客户端与服务器端都调用read和write函数,实际上回声客户端将100%接受自己传输的数据,只不过接收数据时单位出现问题

回声客户端传输的是字符串,而且是通过调用write函数一次性发送的,之后调用read函数,等待接收自己传输的字符串

这里的问题就出现在客户端收到所有字符串数据,需要等待多长时间?等待之后调用read函数是否可以一次性读取完毕?

回声客户端问题解决方法

因为可以提前确定接收数据的大小,所以这个问题不难解决,如果之前传输了100字节长的字符串,则在接收时循环调用read函数读取100字节结课

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_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"))
			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);
}

在之前的示例中只调用了一次read函数,上述示例为了接收所有传输的数据而循环调用read函数

c++ 复制代码
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;
}

如果将上述代码改成:

c++ 复制代码
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_len中保存的数据等于str_len中保存的数据,即可结束while循环,读者们肯定认为这种循环写法更符合逻辑,但是可能因为某种异常情况导致死循环,所以尽可能的使用 while(recv_len<str_len) ,即使发生异常情况也不会导致无限循环

TCP原理

TCP套接字中的I/O缓冲

TCP套接字的数据收发无边界,服务器端即使调用一次write函数传输40字节的数据,客户端也有可能通过4次read函数调用每次读取10字节,但是问题又出现了,服务器端一次性传输了40字节,而客户端居然可以缓慢的分批接收,,客户端接收10字节后,剩下的30字节在什么地方等着呢?

事实上,write函数调用后并非立即传输数据,read函数调用后也并非马上接收数据。更准确的来说,write函数调用瞬间,数据将移到输出缓冲;read函数调用瞬间,从输入缓冲读取数据。

调用write函数时,数据将移到输出缓冲,在适当的时候传向对方的输入缓冲,这时对方调用read函数从输入缓冲读取数据

I/O缓冲特性如下

  • I/O缓冲在每个TCP套接字中单独存在
  • I/O缓冲在创建套接字时自动生成
  • 即使关闭套接字也会继续传递输出缓冲中遗留的数据
  • 关闭套接字将丢失输入缓冲中的数据

TCP内部工作原理1:与对方套接字的连接

TCP套接字从创建到消失所经过程分为三步:

1.与对方套接字建立连接

2.与对方套接字交换数据

3.断开与对方套接字的连接

TCP在实际通信过程中也会经过三次对话过程,因此该过程又称为三次握手

套接字是以全双工方式工作的,也就是说明套接字可以双向传递数据

1、 请求连接的主机A向主机B传递如下信息:

[SYN] SEQ: 1000, ACK: -

该消息中SEQ为1000,ACK为空,SEQ为1000的含义如下:

现在传递的数据包序号为100,如果接收无误,请通知我向你传递1001号数据包

2、 这是首次请求连接时使用的消息,又称SYN,表示收发数据前传输的同步消息,接下来主机B向A传递如下消息:

[SYN+ACK] SEQ: 2000, ACK: 1001

此时SEQ为2000,ACK为1001,而SEQ为2000的含义如下:

现在传递的数据包序号为2000,如果接收无误,请通知我向你传递2001号数据包

而ACK 1001的含义如下:

刚才传输的SEQ为1000的数据包接收无误,现在请传递SEQ为1001的数据包

对主机A首次传输的的数据包确认消息(ACK 1001)和为主机B传输数据做准备的同步消息(SEQ 200)捆绑发送,因此此种类型的消息又称为 SYN + ACK

3、 收发数据前向数据包分配序号,并向对方通报此序号,这都是为了防止数据丢失所做的准备。通过向数据包分配序号并确认,可以在数据丢失时马上查看并重传丢失的数据包,所以TCP可以保证可靠的数据传输。最后观察主机A向B传输的消息:

[ACK] SEQ : 1001, ACK: 2001

TCP连接过程中发送数据包时需要分配序号,在此之前的序号100的基础上+1,此时该数据包传递如下消息:

已正确收到传输的SEQ为2000的数据包,现在可以传输SEQ为2001的数据包

TCP内部工作原理2:与对方主机的数据交换

通过第一步三次握手过程完成了数据交换准备,下面就开始正式收发数据

首先主机A通过1个数据包发送100个字节的数据,数据包的SEQ为1200,主机B为了确认这点,向主机A发送了ACK1301消息

此时的ACK号为1301而非1201,原因在于ACK号的增量为传输的数据字节数,假设每次ACK号不加传输的字节数,这样虽然可以确认数据包的传输,但是不能明确100字节却不正确传递还是丢失,因此按照如下公式传递ACK消息:

ACK 号 --- SEQ号 + 传递的字节数 + 1

与三次握手协议相同,最后+1是为了告知对方下次要传递的SEQ号

下面分析传输过程中数据包消失的情况

SEQ1301数据包向主机B传递100字节数据,但中间发生错误,主机B未收到,一段时间后,主机A仍未收到对于SEQ 1301的ACK确认,因此重传改数据包,为了完成数据包重传,TCP套接字启动计时器以等待ACK回答,相应计时器发生超时则重传

TCP内部工作原理3:断开与套接字的连接

先由套接字A向套接字B传递断开连接的消息,套接字B发出确认收到的消息,然后向套接字A传递可以断开连接的消息,套接字A同样发出确认消息

数据包内的FIN表示断开连接,双方各发送一次FIN消息后断开连接,此过程经历四个阶段,因此又称为四次握手

总结

这是《TCP/IP网络编程》专栏的第三篇文章,欢迎各位读者订阅!

更多资料点击 添加链接描述 欢迎各位读者去Star

⭐学术交流群Q 754410389 持续更新中~~~

相关推荐
PcVue China2 小时前
PcVue + SQL Grid : 释放数据的无限潜力
大数据·服务器·数据库·sql·科技·安全·oracle
长安11083 小时前
前后端、网关、协议方面补充
网络
舞动CPU4 小时前
linux c/c++最高效的计时方法
linux·运维·服务器
钰@5 小时前
小程序开发者工具的network选项卡中有某域名的接口请求,但是在charles中抓不到该接口
运维·服务器·小程序
wanhengwangluo5 小时前
云服务器和物理服务器的区别有哪些?
运维·服务器
hzyyyyyyyu5 小时前
隧道技术-tcp封装icmp出网
网络·网络协议·tcp/ip
南猿北者6 小时前
docker Network(网络)
网络·docker·容器
Hacker_Nightrain7 小时前
网络安全CTF比赛规则
网络·安全·web安全
扣得君7 小时前
C++20 Coroutine Echo Server
运维·服务器·c++20
网络安全指导员8 小时前
恶意PDF文档分析记录
网络·安全·web安全·pdf