第5章 基于TCP的服务器端和客户端(2)

5.1回声客户端的完美实现

1.回声服务端没有问题,只有回声客户端有问题?

下面是之前客户端的代码

while(1)
{
    fputs("Input message(Q to quit): ",stdout);
    fgets(message.BUF_SIZE,stdin);
    ...
    write(sock,message,strlen(message));
    str_len = read(sock,message,BUF_SIZE -1);
    message[str_len] = 0;
    printf("Message from server : %s",message);
}

回声客户端传输的是字符串,而且是通过调用write函数一次性发送的。之后调用read函数,期待一次接收自己传输的字符串。既然回声客户端会收到所有字符串数据,是否多等一会?过段时间再调用read函数是否能一次性读出所有的字符串数据?
的确过段时间再接收即可,但是要等多久?这不符合常理,理想的客户端应该是收到字符串数据时立即读出并输出。

2.回声客户端问题解决方法

#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函数,接收的数据大小应该会和传输的相同,也就是说recv_len等于str_len时就跳出while循环,但是循环条件写等于容易导致无限循环,加入接收的数据出现异常recv_len超过了str_len就会无限循环,所以条件写成“recv_len<str_len”,可以避免陷入无限循环。

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

回声客户端可以提前知道接收的数据长度,但是更多情况下这不太可能。那么,若无法预知接收的数据长度该如何收发数据?这就需要的就是应用层协议的定义。
只要在收发数据过程中定好规则以表示数据边界,或提前告知收发数据的大小。服务端和客户端实现过程中逐步定义的这些规则就是应用层协议

4.计算器服务端和客户端示例

先设计一下协议

  • 客户端连接到服务器端后以一字节整数形式传递待算数字个数

  • 客户端向服务器端传递的每个整型数据占用4字节

  • 传递整型数据后接着传递运算符。运算符信息占用1字节

  • 选择字符+、-、*之间传递

  • 服务端以4字节整型向客户端传回运算结果

  • 客户端得到运算结果后终止与服务器端的连接

      这种程度的协议就相当于实现了一半的程序,这也说明应用层协议在网络编程中的重要性。
      之前说过调用close函数将向对方传递EOF,要记住这一点并利用。
    

客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
#define RLT_SIZE 4  //运算结果字节数设为常数
#define OPSZ 4      //待算数字字节数设为常数
void error_handling(char *message);

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;  /*从程序用户的输入中得到待算数的个数,保存到数组opmsg中。强制转换为char类型,因为协议规定待算个数通过一字节整数型传递,因此输入不能
         超过一个字节整数型能表示的范围(否则整型转化为char类型会丢失精度)。示例中用的是有符号整型,但是个数不可能
         小于0,所以无符号整型更合理。
         */

	
	for(i=0; i<opnd_cnt; i++)
	{
		printf("Operand %d: ", i+1);
		scanf("%d", (int*)&opmsg[i*OPSZ+1]);
	}/*从程序用户的输入中得到待算整数,保存到opmsg中。4字节int型数据要保存到char数组中,因而转化为int类型指针。
    (先取得要存输数据的位置的首地址转化为int类型指针,编译器会自动按照输入的整型4字节存储到该地址后面连续的四个字节空间内)*/
	fgetc(stdin);//前面输入完会有一个'/n',把缓冲中的'/n'给取出来,以便后面输入运算符信息。

	fputs("Operator: ", stdout);
	scanf("%c", &opmsg[opnd_cnt*OPSZ+1]);//输入运算符信息
	write(sock, opmsg, opnd_cnt*OPSZ+2);//传输opmsg数组中运算信息
	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);
}

存储结构

服务端:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
#define OPSZ 4
void error_handling(char *message);
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++)//为了接收5个客户端的连接请求
	{
		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 op)
{
	int result=opnds[0], i;
	
	switch(op)
	{
	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);
}

5.2TCP原理

1.TCP套接字中的I/O缓冲

假设服务端一次传输了40字节,客户端缓慢分批接收。客户端接收10字节以后,其他的30字节在哪里等待呢?

实际上write函数调用后并非立即传输数据,read调用后也并非立即接受数据。
write调用后,数据将移动到输出缓冲;read调用后,从输入缓冲读取数据。

I/O缓冲特性:

  • I/O缓冲在每个TCP套接字中单独存在

  • I/O缓冲在创建套接字时会自动生成

  • 即使关闭套接字也会继续传递输出缓冲中遗留的数据

  • 关闭套接字将丢失输入缓冲中的数据

"可能的问题:客户端输入缓冲为50字节,而服务器传输100字节"
当然这是不可能发生的,不可能发生超过输入缓冲大小的数据传输 因为TCP会控制数据流。TCP中有滑动窗口(Silding Window)协议:

套接字A:“你好你可以传送50字节向我”
套接字B:“OK”

套接字A:“我腾出了20字节的空间,最多可以接收70字节”
套接字B:“OK!”

write函数和Windows中的send函数并不会在向对方主机完成数据传输时返回,而是数据移动到输出缓冲时,就返回。因为TCP会保证对输出缓冲数据的传输,所以我们才说write函数在数据传输完成时返回。

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

  • 与对方套接字建立连接

  • 与对方套接字进行数据交换

  • 断开与对方套接字的连接

建立连接的过程中有三次对话过程,实际TCP在通信过程中也会经过3次对话过程,该过程又称为Three-way handshaking(三次握手) TCP套接字连接过程

套接字是以全双工方式工作的。也就是说双向传递数据。因此收发数据前要做一些准备。

[SYN] SEQ: 1000,ACK:-

该消息的含义:现在传递数据包序号为1000,如果接受无误,清通知我向您传递1001号数据包。 首次的连接请求使用的消息称为:SYN。(Synchronization),表示收发数据前传输的同步消息

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

SEQ:2000 现在传递的数据包序号为2000,如果接受无误请通知我向您传递2001号数据包。 ACK:1001 刚才您传输的SEQ为1000的数据包接受无误,现请传递SEQ为1001的数据包。

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

收发数据前向数据包分配序号,并向对方通报此序号,这都是为了防止数据丢失所做的准备。
通过向数据包分配序号并确认,可以在数据丢失时马上查看并重传丢失的数据包,所以TCP可以保证可靠传输。

[ACK] SEQ:1001, ACK:2001

TCP传输过程要为数据包分配序号,所以+1就成了1001,这条消息代表已经正确接收到传输的SEQ为2000的数据包,现在可以传输SEQ为2001的数据包

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

前面三次握手完成了数据传输前的准备,后面就是收发数据

主机A用一个数据包发送了100个字节的数据,数据包的SEQ为1200.主机B为了确定这一点,向主机A发送ACK为1301的消息,此时的ACK为1301而不是1201的原因是ACK号的增量为传输的数据字节数。如果不加传输的字节数,就无法确定100字节的数据是否正确的传递。最后加1是为了告知对方下次要传递的 SEQ号。

数据包丢失的情况 为了防止数据包传输中的丢失,TCP在发送数据包后会启动超时重传计时器等待ACK应答,如果计时器超过某个值就会重传。

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

如果在数据传输过程中直接断掉连接会出问题,所以断开连接要双方协商。

套接字A:"我希望断开连接"
套接字B:“哦是吗?请稍等,我还有数据要传输”


套接字B:"数据传输完成,我也准备就绪,可以断开连接"
套接字A:"谢谢合作"

双方各发送一次FIN消息后断开连接。此过程经历四个阶段,称为四次握手(Four-way handshaking)

图中向主机传递了两次ACK 5001可能会有疑惑,实际是第二次FIN数据包中的ACK 5001只是因为接收ACK消息后未接收到数据而超时重传的。

5.3基于Windows的实现

几乎与Linux相同,只不过需要初始化套接字版本库和注销版本库,代码的变量变成Windows风格的变量

服务端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#define BUF_SIZE 1024
#define OPSZ 4
void ErrorHandling(char *message);
int calculate(int opnum, int opnds[], char oprator);

int main(int argc, char *argv[])
{
	WSADATA wsaData;
	SOCKET hServSock, hClntSock;
	char opinfo[BUF_SIZE];
	int result, opndCnt, i;
	int recvCnt, recvLen;	
	SOCKADDR_IN servAdr, clntAdr;
	int clntAdrSize;
	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
	
	if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
		ErrorHandling("WSAStartup() error!"); 
	
	hServSock=socket(PF_INET, SOCK_STREAM, 0);   
	if(hServSock==INVALID_SOCKET)
		ErrorHandling("socket() error");
	
	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family=AF_INET;
	servAdr.sin_addr.s_addr=htonl(INADDR_ANY);
	servAdr.sin_port=htons(atoi(argv[1]));

	if(bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr))==SOCKET_ERROR)
		ErrorHandling("bind() error");
	if(listen(hServSock, 5)==SOCKET_ERROR)
		ErrorHandling("listen() error");	
	clntAdrSize=sizeof(clntAdr);

	for(i=0; i<5; i++)
	{
		opndCnt=0;
		hClntSock=accept(hServSock, (SOCKADDR*)&clntAdr, &clntAdrSize);	
		recv(hClntSock, &opndCnt, 1, 0);
		
		recvLen=0;
		while((opndCnt*OPSZ+1)>recvLen)
		{
			recvCnt=recv(hClntSock, &opinfo[recvLen], BUF_SIZE-1, 0);
			recvLen+=recvCnt;
		}
		result=calculate(opndCnt, (int*)opinfo, opinfo[recvLen-1]);
		send(hClntSock, (char*)&result, sizeof(result), 0);
		closesocket(hClntSock);
	}
	closesocket(hServSock);
	WSACleanup();
	return 0;
}

int calculate(int opnum, int opnds[], char op)
{
	int result=opnds[0], i;
	
	switch(op)
	{
	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 ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#define BUF_SIZE 1024
#define RLT_SIZE 4
#define OPSZ 4
void ErrorHandling(char *message);

int main(int argc, char *argv[])
{
	WSADATA wsaData;
	SOCKET hSocket;
	char opmsg[BUF_SIZE];
	int result, opndCnt, i;
	SOCKADDR_IN servAdr;
	if(argc!=3) {
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	
	if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
		ErrorHandling("WSAStartup() error!"); 

	hSocket=socket(PF_INET, SOCK_STREAM, 0);   
	if(hSocket==INVALID_SOCKET)
		ErrorHandling("socket() error");
	
	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family=AF_INET;
	servAdr.sin_addr.s_addr=inet_addr(argv[1]);
	servAdr.sin_port=htons(atoi(argv[2]));
	
	if(connect(hSocket, (SOCKADDR*)&servAdr, sizeof(servAdr))==SOCKET_ERROR)
		ErrorHandling("connect() error!");
	else
		puts("Connected...........");

	fputs("Operand count: ", stdout);
	scanf("%d", &opndCnt);
	opmsg[0]=(char)opndCnt;
	
	for(i=0; i<opndCnt; i++)
	{
		printf("Operand %d: ", i+1);
		scanf("%d", (int*)&opmsg[i*OPSZ+1]);
	}
	fgetc(stdin);
	fputs("Operator: ", stdout);
	scanf("%c", &opmsg[opndCnt*OPSZ+1]);
	send(hSocket, opmsg, opndCnt*OPSZ+2, 0);
	recv(hSocket, &result, RLT_SIZE, 0);
	
	printf("Operation result: %d \n", result);
	closesocket(hSocket);
	WSACleanup();
	return 0;
}

void ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

本文章使用limfx的vscode插件快速发布