下面是之前客户端的代码
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函数是否能一次性读出所有的字符串数据?
的确过段时间再接收即可,但是要等多久?这不符合常理,理想的客户端应该是收到字符串数据时立即读出并输出。
#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”,可以避免陷入无限循环。
回声客户端可以提前知道接收的数据长度,但是更多情况下这不太可能。那么,若无法预知接收的数据长度该如何收发数据?这就需要的就是应用层协议的定义。
只要在收发数据过程中定好规则以表示数据边界,或提前告知收发数据的大小。服务端和客户端实现过程中逐步定义的这些规则就是应用层协议
先设计一下协议:
客户端连接到服务器端后以一字节整数形式传递待算数字个数
客户端向服务器端传递的每个整型数据占用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);
}
假设服务端一次传输了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函数在数据传输完成时返回。
与对方套接字建立连接
与对方套接字进行数据交换
断开与对方套接字的连接
建立连接的过程中有三次对话过程,实际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的数据包
前面三次握手完成了数据传输前的准备,后面就是收发数据
主机A用一个数据包发送了100个字节的数据,数据包的SEQ为1200.主机B为了确定这一点,向主机A发送ACK为1301的消息,此时的ACK为1301而不是1201的原因是ACK号的增量为传输的数据字节数。如果不加传输的字节数,就无法确定100字节的数据是否正确的传递。最后加1是为了告知对方下次要传递的 SEQ号。
数据包丢失的情况: 为了防止数据包传输中的丢失,TCP在发送数据包后会启动超时重传计时器等待ACK应答,如果计时器超过某个值就会重传。
如果在数据传输过程中直接断掉连接会出问题,所以断开连接要双方协商。
套接字A:"我希望断开连接"
套接字B:“哦是吗?请稍等,我还有数据要传输”
套接字B:"数据传输完成,我也准备就绪,可以断开连接"
套接字A:"谢谢合作"
双方各发送一次FIN消息后断开连接。此过程经历四个阶段,称为四次握手(Four-way handshaking)
图中向主机传递了两次ACK 5001可能会有疑惑,实际是第二次FIN数据包中的ACK 5001只是因为接收ACK消息后未接收到数据而超时重传的。
几乎与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插件快速发布