第五章:基于TCP的服务器端/客户端2

第五章:基于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 函数在数据传输完成时返回,指的是将数据移到输出缓冲时就返回了,而非完成向对方主机的数据传输。
  • 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内部的工作原理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消息后未接收数据而重传的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值