第五章:基于TCP的服务器端/客户端2
5、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将数据读完,知道读取数据大小等于传输数据大小即可。
-
while(1){ fputs("Iput message(Q to quit): ",stdout); // 从终端中读入数据到缓存message中。 fgets(message,BUF_SIZE,stdin); if(!strcmp(message,"q\n") || !strcmp(message,"Q\n")) break; // 将从终端读取暂存在message中的数据写入sock中,记录写入的字节数 str_len = write(sock,message,strlrn(message)); // 定义接收到的字节数。 recv_len = 0; // 接收到的字节数小于写入的字节数时,就一直读 while(recv_len < str_len){ // 记录每次调用read读取的字节数 recv_cnt = read(sock,&message[recv_len],BUF_SIZE - 1); if(recv_cnt == -1) error_handling("read() error"); // 累加读取的字节数总数 recv_len += recv_cnt; } // 最后读入的字符后加0,成为字符串。 message[recv_len] = 0; printf("Message from server: %s",message); }
-
-
如果问题不在回声客户端:定义应用层协议
-
如果无法预知接收数据长度时应如何收发数据?此时需要定义应用层协议。回声服务器端/客户端中定义了如下协议“收到Q就立即终止连接。”
-
收发数据过程中也需要定好规则(协议)以表示数据的边界, 或提前告知收发数据的大小。服务器端/客户端实现过程中逐步定义的这些规则集合就是应用层协议。是为特定的程序的实现而制定的规则。
-
应用层协议实现案例:
- 服务器端从客户端获得多个数字和运算符信息;
- 服务器接收到数字后对其进行加减乘运算,然后将结果传回客户端。
-
应用层协议示例:
-
客户端连接到服务器端后以1字节整数形式传递待算数字个数。
-
客户端向服务器端传递的每个整数型数据占用4字节
-
传递整数型数据后传输运算符,运算符信息占用1字节
-
选择字符+、-、* 之一传递
-
服务器端以4字节整数型向客户端传回运算结果
-
客户端得到运算结果后终止于服务器的连接。
-
#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); // 从用户输入中得到待算个数后,保存到数组opmsg,强制转换为char类型,协议中使用1个字节整数型传递。 scanf("%d",&opnd_cnt); opmsg[0] = (char) opnd_cnt; for(i = 0; i < opnd_cnt; i++){ printf("Operand %d : ", i+1); // 从用户处得到待算整数保存到数组opmsg,4字节int型数据要保存到char数组,因而转换成int指针类型。 scanf("%d",(int*)&opmsg[i*OPSZ + 1]); } fgetc(stdin); fputs("Operator: ",stdout); scanf("%c",&opmsg[opnd_cnt*OPSZ + 1]); // 调用write函数一次性传输opmsg数组中的运算相关信息。 opmsg中保存一个字节的数字总数,opnd_cnt个4字节的数字,以及一个字节的运算符。 write(sock,opmsg,opnd_cnt *OPSZ + 2); // 保存服务器端传输的运算结果,待接收的数据长度为4字节,调用1次read函数即可接收。 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); }
- 要想在同一数组中保存并传输多种数据类型,应把数组声明为char类型,而且需要额外做一些指针及数组运算。
-
-
服务器端示例:
-
#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 caculate(int opnum, int opnds[],char operator); 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++){ opnd_cnt = 0; clnt_sock = accept(serv_sock,(struct sockaddr*)&clnt_adr, &clnt_adr_sz); // 接收待算操作数 read(clnt_sock,&opnd_cnt,1); printf("%d \n",opnd_cnt); 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 = caculate(opnd_cnt,(int*)opinfo,opinfo[recv_len-1]); write(clnt_sock,(char*)&result,sizeof(result)); close(clnt_sock); } // 完成5个客户端提供服务后关闭服务器端套接字并终止程序 close(serv_sock); return 0; } void error_handling(char* message){ fputs(message,stderr); fputc('\n',stderr); exit(1); } int caculate(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; }
-
-
5、2 TCP 原理:
- TCP套接字中的I/O缓冲:
- write函数调用后并非立即传输数据,read函数调用后也并非立马接收数据,write函数调用瞬间,数据将移至输出缓冲,read函数调用瞬间,从输入缓冲中读取数据。
- I/O缓冲特性:
- I/O缓冲在每个TCP套接字中单独存在。
- I/O缓冲在创建套接字时自动生成。
- 即使关闭套接字也会继续传递输出缓冲中遗留的数据。
- 关闭套接字将丢失输入缓冲中的数据。
- 不会发生超过输入缓冲大小的数据传输。TCP中有滑动窗口协议,不会因为缓冲溢出而丢失数据。
- write 函数在数据传输完成时返回,指的是将数据移到输出缓冲时就返回了,而非完成向对方主机的数据传输。
- write函数调用后并非立即传输数据,read函数调用后也并非立马接收数据,write函数调用瞬间,数据将移至输出缓冲,read函数调用瞬间,从输入缓冲中读取数据。
- TCP内部工作原理1:与对方套接字的连接:
- TCP套接字从创建到消失的过程:
- 与对方套接字建立连接
- 与对方套接字进行数据交换
- 断开与对方套接字的连接
- 与对方套接字建立连接:
- TCP在通信中也会经历三次对话过程:称为Tree-way handshaking(三次握手)。
- 套接字是以双全工方式工作的,即它可以双向传递数据。
- 请求连接的主机A向主机B传递信息:
- [SYN] SEQ: 1000, ACK: -
- 消息中SEQ为1000, ACK为空;SEQ为1000的含义:传递的数据包序号为1000,若接收无误,请通知我向您传递1001号数据包。
- 首次请求连接时使用的消息,又称SYN即Synchronization的简写,表示收发数据前传输的同步消息。
- 主机B向A传递的消息:
- [SYN + ACK] SEQ: 2000, ACK: 1001
- ACK(Acknowledge character)确认字符,数据通信中,接收站发给发送站的一种传输类控制字符,表示发来的数据已确认接收无误。
- SEQ为2000,ACK为1001,SEQ是2000的含义:现传递的数据包序号是2000,接收无误,请通知我向你传递2001号数据包,ACK 1001的含义:刚才传输的SEQ为1000的数据包无误,请传递SEQ为1001的数据包。
- 为主机A首次传输的数据包的确认信息(ACK 1001)和为主机B传输数据做准备的同步消息(SEQ 2000捆绑发送,这种消息称为 SYN + ACK)
- 主机A向主机B传输的消息:
- [ACK] SEQ:1001, ACK:2001
- TCP连接过程中发送数据包时需要分配序号,已正确收到传输的SEQ为2000的数据包,可以传输SEQ为2001的数据包。
- 此时,主机A和主机B确认了彼此就绪。
- TCP在通信中也会经历三次对话过程:称为Tree-way handshaking(三次握手)。
- TCP套接字从创建到消失的过程:
- TCP内部的工作原理2:与对方主机的数据交换:
-
三次握手完成了数据交换准备,可以开始收发数据。
-
主机A分两个数据包向主机B传递了200字节的过程:
- 首先,主机A通过一个数据包发送100字节的数据,数据包的SEQ为1200,主机B为了确认,向主机A发送ACK 1301消息,ACK是1301而不是1201是因为ACK号的增量为传输的数据字节数。
- ACK号 ----> SEQ 号 + 传递的字节数 + 1
- 最后加1是为了告知对方下次要传递的SEQ号。
-
传输过程中数据包消失的情况:
- TCP套接字启动计时器以等待ACK的应答,若计时器发生了超时,则重传。
-
- TCP的内部工作原理3:断开与套接字的连接:
-
如果对方还有数据需要传输时直接断掉连接会出问题,所以断开连接时需要双方协商。
-
先由套接字A向套接字B传递断开连接的消息,套接字B发出确认收到的消息,然后向套接字A传递可以断开的消息,套接字A同样发出确认消息。
-
数据包中FIN表示断开连接,双方各发送1次FIN消息后断开连接,这个过程又称为四次握手(Four-way handshaking),向主机A传递了两次ACK 5001,是因为接收ACK消息后未接收数据而重传的。
-