基于TCP协议开发文件传输系统一


简介:基于TCP协议开发文件传输系统,搭建多进程网络服务程序框架,实现TCP长连接心跳机制、文件上传与下载、异步通信实现快速传输。

socket通讯

什么是Socket?
Socket(套接字),用来描述IP地址和端口,是通信链的句柄,是支持TCP/IP协议的网络通信的基本操作单元,是对网络通信过程中端点的抽象表示。
通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。Socket()函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。

插述:
TCP(Transmission Control Protocol)传输控制协议,是一种面向连接的、可靠的、基于字节流的通信协议。数据在传输前要建立连接,传输完毕后还要断开连接。客户端在收发数据前要使用 connect() 函数和服务器建立连接。建立连接的目的是保证IP地址、端口、物理链路等正确无误,为数据的传输开辟通道。
在这里插入图片描述

TCP粘包问题

socket缓冲区和数据的传递过程,可以看到数据的接收和发送是无关的,read()/recv() 函数不管数据发送了多少次,都会尽可能多的接收数据。也就是说,read()/recv() 和 write()/send() 的执行次数可能不同。

例如,write()/send() 重复执行三次,每次都发送字符串"abc",那么目标机器上的 read()/recv() 可能分三次接收,每次都接收"abc";也可能分两次接收,第一次接收"abcab",第二次接收"cabc";也可能一次就接收到字符串"abcabcabc"。

假设我们希望客户端每次发送一位学生的学号,让服务器端返回该学生的姓名、住址、成绩等信息,这时候可能就会出现问题,服务器端不能区分学生的学号。例如第一次发送 1,第二次发送 3,服务器可能当成 13 来处理,返回的信息显然是错误的。

这就是数据的“粘包”问题,客户端发送的多个数据包被当做一个数据包接收。也称数据的无边界性,read()/recv() 函数不知道数据包的开始或结束标志(实际上也没有任何开始或结束标志),只把它们当做连续的数据流来处理。
在这里插入图片描述
测试粘包:
在这里插入图片描述

TCP粘包问题解决

采用自定义的报文格式:报文长度+报文内容
在这里插入图片描述
在这里插入图片描述

(*ibuflen) = 0;  // 报文长度变量初始化为0。
  // 先读取报文长度,4个字节。
  if (Readn(sockfd,(char*)ibuflen,4) == false) return false;
  (*ibuflen)=ntohl(*ibuflen);  // 把报文长度由网络字节序转换为主机字节序。
  // 再读取报文内容。
  if (Readn(sockfd,buffer,(*ibuflen)) == false) return false;

关于TCP缓冲区

什么是tcp缓冲区?
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。

缓冲区的意义
write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,比如nagle算法,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。
read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。

I/O缓冲区特性
1、I/O缓冲区在每个TCP套接字中单独存在;
2、I/O缓冲区在创建套接字时自动生成;
3、即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
4、关闭套接字将丢失输入缓冲区中的数据。

输入输出缓冲区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取:

int servSock = socket(PF_INET, SOCK_STREAM, 0);
unsigned optVal;
int optLen = sizeof(int);
getsockopt(servSock, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen);
printf("Buffer length: %d\n", optVal);

运行结果:
Buffer length: 8192

封装socket常用函数

bool CTcpServer::InitServer(const unsigned int port,const int backlog)
{
  // 如果服务端的socket>0,关掉它,这种处理方法没有特别的原因,不要纠结。
  if (m_listenfd > 0) { close(m_listenfd); m_listenfd=-1; }

  if ( (m_listenfd = socket(AF_INET,SOCK_STREAM,0))<=0) return false;

  // 忽略SIGPIPE信号,防止程序异常退出。
  signal(SIGPIPE,SIG_IGN);   

  // 打开SO_REUSEADDR选项,当服务端连接处于TIME_WAIT状态时可以再次启动服务器,
  // 否则bind()可能会不成功,报:Address already in use。
  //char opt = 1; unsigned int len = sizeof(opt);
  int opt = 1; unsigned int len = sizeof(opt);
  setsockopt(m_listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,len);    

  memset(&m_servaddr,0,sizeof(m_servaddr));
  m_servaddr.sin_family = AF_INET;
  m_servaddr.sin_addr.s_addr = htonl(INADDR_ANY);   // 任意ip地址。
  m_servaddr.sin_port = htons(port);
  if (bind(m_listenfd,(struct sockaddr *)&m_servaddr,sizeof(m_servaddr)) != 0 )
  {
    CloseListen(); return false;
  }

  if (listen(m_listenfd,backlog) != 0 )
  {
    CloseListen(); return false;
  }

  return true;
}

bool CTcpServer::Accept()
{
  if (m_listenfd==-1) return false;

  m_socklen = sizeof(struct sockaddr_in);

  if ((m_connfd=accept(m_listenfd,(struct sockaddr *)&m_clientaddr,(socklen_t*)&m_socklen)) < 0)
      return false;

  return true;
}

析构函数中会释放socket连接。

多进程网络服务程序框架

TCP长连接与短连接

1、使用方法不同。长连接是client方与server方先建立连接,连接建立后不断开,然后再进行报文发送和接收。短连接是Client方与server每进行一次报文收发交易时才进行通讯连接,交易完毕后立即断开连接。此方式常用于一点对多点通讯

2、操作过程不同。长连接的操作步骤是:建立连接、数据传输…、保持连接、数据传输、关闭连接。短连接的操作步骤是:建立连接、数据传输、关闭连接、建立连接、数据传输、关闭连接。

3、使用时机不同。长连接:短连接多用于操作频繁,点对点的通讯,而且长连接数不能太多的情况。每个TCP连接的建立都需要三次握手,每个TCP连接的断开要四次握手。

在HTTP/1.0中,默认使用的是短连接,也就是说,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。如果客户端浏览器访问的某个HTML或其他类型的Web页中包含有其他的Web资源,如js文件、图像文件、CSS文件等;当浏览器每遇到这样一个Web资源,就会建立一个HTTP会话。

但从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头中加入 Connection:keep-alive

在这里插入图片描述
心跳机制:心跳机制是定时发送一个自定义的结构体(心跳包),让对方知道自己还活着,以确保连接的有效性的机制。
客户端心跳:以xml格式向客户端发送心跳包

// 心跳。 
bool srv000()    
{
  char buffer[1024];
 
  SPRINTF(buffer,sizeof(buffer),"<srvcode>0</srvcode>");
  printf("发送:%s\n",buffer);
  if (TcpClient.Write(buffer)==false) return false; // 向服务端发送请求报文。

  memset(buffer,0,sizeof(buffer));
  if (TcpClient.Read(buffer)==false) return false; // 接收服务端的回应报文。
  printf("接收:%s\n",buffer);

  return true;
}

多进程的服务程序

在这里插入图片描述
一个服务端响应客户端,服务端程序不会退出。
子进程处理业务,父进程继续accept,接受客户端的请求。

加入子进程,解决了子进程不能退出,在循环内退出后,会产生僵尸进程
解决僵尸进程:信号处理函数中,加入single()函数,则父进程不需要等待。

while (true)
  {
    // 等待客户端的连接请求。
    if (TcpServer.Accept()==false)
    {
      logfile.Write("TcpServer.Accept() failed.\n"); FathEXIT(-1);
    }

    logfile.Write("客户端(%s)已连接。\n",TcpServer.GetIP());

    if (fork()>0) { TcpServer.CloseClient(); continue; }  // 父进程继续回到Accept()。
   
    // 子进程重新设置退出信号。
    signal(SIGINT,ChldEXIT); signal(SIGTERM,ChldEXIT);

    TcpServer.CloseListen();

    // 子进程与客户端进行通讯,处理业务。
    char buffer[102400];

    // 与客户端通讯,接收客户端发过来的报文后,回复ok。
    while (1)
    {
      memset(buffer,0,sizeof(buffer));
      if (TcpServer.Read(buffer)==false) break; // 接收客户端的请求报文。
      logfile.Write("接收:%s\n",buffer);

      strcpy(buffer,"ok");
      if (TcpServer.Write(buffer)==false) break; // 向客户端发送响应结果。
      logfile.Write("发送:%s\n",buffer);
    }

    ChldEXIT(0);

扩展:僵尸进程

在这里插入图片描述
在这里插入图片描述
僵尸进程处理的三种方式

  1. 内核向父进程发送SIGCHLD信号,
    2.wait函数
    但父进程会阻塞
    3.在信号处理函数中,调用wait,不需要等待
    在这里插入图片描述

Linux一切皆文件

Linux中所有内容都是以文件的形式保存和管理,即:一切皆文件。
普通文件是文件。
目录(在win下称为文件夹)是文件。
硬件设备(键盘、硬盘、打印机)是文件。
套接字(socket)、网络通信等资源也都是文件。

启动一个 linux 进程时,程序默认打开三个 I/O 设备文件:标准输入文件 stdin,标准输出文件 stdout,标准错误输出文件 stderr,而每打开一个文件,就有一个代表该打开文件的文件描述符(比较小的整数)。而一般情况下,stdin、stdout 和 stderr 对应的文件描述符就是 0、1、2。
在这里插入图片描述
查看文件描述符:
在这里插入图片描述
fd打开越多,则资源消耗越大,所以在工程中,总是关闭可以关闭的fd.

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

玖玖玖_violet

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值