IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> 基于TCP的服务器端、客户端(下) -> 正文阅读

[网络协议]基于TCP的服务器端、客户端(下)

这一章是在上一章的基础上写的,如果没看过,请移步基于TCP的服务器端、客户端(上),通过这一章你会更加理解为什么网络编程里面需要许多协议,以及明白各种协议的使用是为了解决各种特殊的问题,回声测试就是一个很好的说明例子。
注意本文所有代码测试环境是在Linux系统下,而非windows

一、回声测试问题解决

1.1 检查问题

我们在上一章分析过问题,所以这里不再赘述;但我们需要知道的是服务端是没有问题问题出在客户端。

下面代码是上一章客户端与服务端部分代码

//服务端
while((str_len=read(clnt_sock,message,BUF_SIZE))!=0)
    write(clnt_sock,message,str_len);

//客户端
  while(1)
    {
        fputs("Input message(Q to quit):",stdout);
        fgets(message,BUF_SIZE,stdin);
        if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
        {
            printf("testing......\n");
               break;
        }
     
        write(sock,message,strlen(message));
        str_len=read(sock,message,BUF_SIZE-1);
        message[str_len]=0;
        printf("Message from server:%s",message);
       
    }

两者都是通过循环调用read或者write函数实现,注意观察客户端,通过write一次性发送一串字符串,并期望接收自己刚刚发出去的字符串并通过调用一次read()将字符串读出来, 因此客户端是接收数据的单位是串,而非字符数,问题就出在这,同时这里预设了一个前提:客户端知道自己会接收怎样的字符串。但是在实际场合中,客户端并不知道自己会接收到怎样的字符串,也许是一串,也许是多串。但是假如收到多串字符串,客户端是否应该等到所有字符串接收完毕再进行读取呢?但是客户端怎么知道等多久?因此比较现实点的做法就是:数据来了我就读取并输出。由于我们暂时没有介绍多进程概念(我会在后面进行补充,有需要可以关注我!),所以用其他方法代替实现。

1.2 客户端解决方法

将接收数据的单位由串改为字节,由于数据发出去的时候已经能算出将要接收数据的字节,因此若之前传输了20字节长的字符串,则在接收时循环调用read函数读取20个字节即可,以下是客户端修改之后的代码(服务端代码不变),你可能在想实际上客户端并不知道要接收到多少数据,注意这是一个过渡的解决方案。

//client.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;
    int 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"))
        {
            printf("testing......\n");
               break;
        }
     
        str_len=write(sock,message,strlen(message));
        recv_len=0;
        while(recv_len<str_len)      //通过接收到的字节判断是否应该继续接收
        {
         recv_cnt= read(sock,message,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()函数可能调用了多次保证了数据能够接收完毕,同时满足“TCP不存在数据边界”特点

1.3 高阶解决方法

上面的解决方案还有一个漏洞,正如我在上一篇博客里面讲的,客户端实际上是无法知道自己要接收到多少数据的,若无法预知接收数据长度时应如何收发数据?此时需要的就是应用层协议的定义,通过定义协议,来确定数据的边界,或者提前告知数据的大小。(服务端、客户端实现过程中逐步定义的这些规则集合就是应用层协议)我们下面通过一个小实验体验一下应用层协议定义过程。

实验过程

服务端从客户端获得多个数字和运算符信息,服务器端收到数字后对其进行加减乘运算,然后把结果返回到客户端并显示。

1.4 计算机服务端、客户端实例

  • 头文件

    #include<stdio.h>
    #include <stdlib.h>
    #include<arpa/inet.h>
    #include<sys/socket.h>
    #include<unistd.h>
    #include<string.h>
    #define BUF_SIZE 1024   
    #define RLT_SIZE 4     //待算数字的字节数
    #define OPSZ 4         //运算结果的字节数
    void error_handling(char *message);
    
  • 客户端实现

    #include "internet.h"
    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; 
        for(int i=0;i<opnd_cnt;i++)
        {
            printf("Operand  %d:",i+1);
            scanf("%d",(int *)&opmsg[i*OPSZ+1]);
        }
        fgetc(stdin);  //删除之前输入时的/n字符也就是enter
        fputs("OPerator: ",stdout);
        scanf("%c",&opmsg[opnd_cnt*OPSZ+1]);
        /*
        调用wirte函数一次性进行传输opmsg中的信息,也可以调用多次,因为TCP中不存在数据边界。
        */
        write(sock,opmsg,opnd_cnt*OPSZ+2);
        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);
    }
    

    我们将操作数、运算符、放在同一个数组opmsg里面,方便数据的传输,该数组的结构如下所示,这也就是客户端代码第24-28行将opmsg强制类型转换的原因:若想在同一数组当中保存并传输多种类型数据,应该把数组声明为char类型。

在这里插入图片描述

  • 服务端实现

    #include"internet.h"
    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++)
      {
          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 oprator)
    {
       int result=opnds[0],i;
       switch (oprator)
       {
       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);
    }
    
  • 实验结果

    先启动服务端:./server 9091

    再启动客户端 ./client 127.0.0.1 9091

    connected....
    Operand count:3
    Operand  1:12
    Operand  2:24
    Operand  3:36
    OPerator: +
    Operation result:72
    

    到这算是成功了。

二、TCP原理

本节目的在于为日后理解套接字(第九章)的基础,可以选择性地看。

2.1 TCP套接字中的I/O缓冲

在之前,或多或少提过TCP套接字的数据收发不存在边界,服务器即使调用一次write函数传输40字节的数据,客户端可能得调用4次read函数每次读取10字节接收数据;但客户端端第一次接收10字节之后,剩下的30字节在何处等待客户端继续调用read函数进行接收呢?难道这30字节在网络数据流当中盘旋等待客户端接收?

实际上,write函数调用之后并非立即将数据传输出去,而是写入缓冲区当中,同样的,read函数并非从网络中读取数据,而是从缓冲区里面读取数据。如下图所示:

在这里插入图片描述

write函数将数据写入缓冲区,在适当的时候传到对方的输入缓冲当中。反之亦然

I/O缓冲特性可以总结如下:

  1. I/0缓冲在每个TCP套接字当中独立存在。
  2. I/O缓冲在创建套接字时自动生成,不需要手动创建。
  3. 即使关闭套接字也会继续传递输出缓冲中遗留的数据。
  4. 关闭套接字将丢失输入缓冲中的数据

那么发送的数据大于输入缓冲区大小,会不会导致数据丢失呢?

答:不会,同样,要解决这个问题,需要制定一套网络协议进行协调;我们比较熟悉的协议例如:TCP中的滑动窗口协议,进行控制数据流。以下对话过程可以大概理解以下协议流程:

套接字A:“您好!,最多可以向我传递50字节的数据”

套接字B:“好的!”

套接字A:“我腾出了20字节的空间,最多可以接收20字节的数据”

套接字B:“好的!”

2.2 TCP原理1(与对方套接字的连接)

TCP从创建到消失经过以下过程:

  1. 建立连接

    建立连接的交互

    套接字A:“您好!套接字B。我这儿有数据传给你,需要和你建立连接”(发送连接请求)

    套接字B:“好的,我这边已经准备好了”(accept 连接)

    套接字A:“谢谢@!”(回复)

    而在实际的建立连接过程当中是通过三次握手完成的。

在这里插入图片描述

套接字是以全双工方式工作的即数据可以双向传递,三次握手流程如下(ACK是确认消息,SEQ是同步消息)

[SYN] : SEQ =1000 ACK= – //SEQ=1000意思是现在传递的数据包序号为1000,如果接收无误,请通 知我向您传递1001号数据包。

[SYN+ACK]:SEQ=2000 ACK=1001 //SEQ=2000表示 现在接收的数据包为2000,如果接收无误,请通知我向你传递传递2001号数据包; ACK=1001 表示 刚刚序号为SEQ=1000数据包接收无误,现在请传递SEQ为1001的数据包。(注意,主机B给主机A发送的确认消息也算数据包

[ACK] : SEQ=1001 ACK=2001 已正确收到传输的SEQ为2000的数据包,现在可以传输SEQ为2001的数据包

自此,主机A,B确认了彼此均就绪。

  1. 数据传输

  2. 断开连接

2.3 TCP原理2(与对方主机的数据交换)

  1. 建立连接

  2. 数据传输

    经过了第一步的三次握手,开始进行数据传输。

    这里展示主机A分两次向B发送200byte数据的过程。

在这里插入图片描述

首先主机A向B发送100byte数据,数据包SEQ为1200,而B为了向A确认收到数据便向主机A发送ACK1301(而不是1201,原因在于ACK号的增加的量为传输的数据字节数,假设每次ACK不加传输的字节数,这样虽然可以确认数据包的传输,但无法明确100字节全部正确传递还是丢失了一部分)ACK计算方法:ACK=SEQ号+传递的字节数+1(加一的目的在于告诉对方下次传递的SEQ号)。

如果出现包的丢失,主机会有一个计时器,如果在固定时间内没有收到发送出去的包的回馈信息,发送方会重新发送包。

  1. 断开连接

2.4 TCP 原理3(断开与套接字的连接)

  1. 建立连接

  2. 数据传输

  3. 断开连接

    先由套接字A向B发送断开连接的信息,套接字B发出确认收到的信息,然后向套接字A传递可以断开的信息,套接字A同样发出确认信息,流程图如下图所示。

在这里插入图片描述

FIN表示断开连接,也就是说双方各发送一次FIN消息之后断开连接。此过程经历四个阶段,又称为四次握手。SEQ与ACK的含义与作用与之前的一样,但是你会发现图中主机B给 主机A传了两次ACK=5001,这是因为接收ACK消息之后未接收数据而重传的。

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-07-17 12:17:20  更:2021-07-17 12:18:17 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/26 23:30:21-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计