Linux网络编程 | Socket编程:TCP服务器单执行流、多线程、多进程的实现


TCP协议

TCP协议:传输控制协议
特性:面向连接,可靠传输,面向字节流
应用场景:安全性要求大于实时性要求的场景---文件传输

TCP通信流程

在这里插入图片描述

Socket相关接口

1.创建套接字
int socket(int domain, int type, int protocol);

2.绑定地址
int bind(int sockfd, struct sockaddr* addr, socklen_t len);

3.开始监听
int listen(int sockfd, int backlog);
   backlog:服务器能够在同一时间处理的最大连接数  

4.客户端发送连接请求
int connect(int sockfd, struct sockaddr* srvaddr, socklen_t len);
   sockfd:套接字描述符
   srvaddr:服务端地址信息
   len:地址长度
   返回值:成功返回0,失败返回-1

5.服务端获取新建连接
int accept(int sockfd, struct sockaddr* cliaddr, socklen_t* addrlen);
   sockfd:监听套接字,服务端最早创建的套接字,只用于接受新的连接请求
   cliaddr:新的连接的客户端地址信息
   addrlen:输入输出参数,指定地址信息长度,以及返回实际长度
   返回值:新建连接的描述符,往后与客户端的通信都通过这个描述符完成

6.收发数据:tcp通信因为socket结构体中包含完整五元组,因此不需要指定地址
ssize_t send(int sockfd, void* data, int len, int flag);
   sockfd:新建套接字描述符
   data:要发送的数据
   len:数据长度
   flag:默认0--阻塞发送
   返回值:成功返回实际发送的数据长度;失败返回1,连接断开会触发异常

ssize_t recv(int sockfd, void* buf, int len, int flag);
   sockfd:新建套接字描述符
   buf:接收数据的空间首地址
   len:要接收的数据长度
   flag:默认0--阻塞接收
   返回值:成功返回实际接收的数据长度;出错返回-1;连接断开返回0

7.关闭套接字
int close(int sockfd); 

TCPSocket的封装

#include<iostream>
using namespace std;
#include<arpa/inet.h>
#include<unistd.h>
#include<cstdio>

#define BACKLOG_MAX 5
#define CHECK(p) if(p==false) {return -1;}

class TcpSocket
{
  public:

    TcpSocket()
      :_sockfd(-1)
    {}

    //创建套接字
    bool Socket()
    {
      _sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
      if(_sockfd < 0)
      {
        perror("create socket error");
        return false;
      }
      return true;
    }


    //绑定地址信息
    bool Bind(const string& ip, const uint16_t& port)
    {
      sockaddr_in addr;
      addr.sin_family = AF_INET;
      addr.sin_port = htons(port);
      addr.sin_addr.s_addr = inet_addr(ip.c_str());
      socklen_t len = sizeof(addr);
      int ret = bind(_sockfd, (sockaddr*)&addr, len);
      if(ret < 0)
      {
        perror("bind error");
        return false;
      }
      return true;
    }

    //开始监听
    bool Listen(int backlog = BACKLOG_MAX)
    {
      int ret = listen(_sockfd, backlog);
      if(ret < 0)
      {
        perror("Listen error");
        return false;
      }
      return true;
    }

    //客户端发送连接请求
    bool Connect(const string& ip, const uint16_t& port)
    {
      sockaddr_in seraddr;//服务端地址信息
      seraddr.sin_family = AF_INET;
      seraddr.sin_port = htons(port);
      seraddr.sin_addr.s_addr = inet_addr(ip.c_str());
      socklen_t len = sizeof(sockaddr_in);
      int ret = connect(_sockfd, (sockaddr*)&seraddr, len);
      if(ret < 0)
      {
        perror("connect failed");
        return false;
      }
      return true;
    }

    //服务端获取新建连接
    bool Accept(TcpSocket& tcpsocket, string* ip = NULL, uint16_t* port = NULL)
    {
      sockaddr_in cliaddr;
      socklen_t len = sizeof(sockaddr_in);
      int ret = accept(_sockfd, (sockaddr*)&cliaddr, &len);
      if(ret < 0)
      {
        perror("accept error");
        return false;
      }
      tcpsocket._sockfd = ret;
      if(ip != NULL)
        *ip = inet_ntoa(cliaddr.sin_addr);
      if(port != NULL)
        *port = ntohs(cliaddr.sin_port);
      return true;
    }

    //发送数据
    //要发送整条数据
    bool Send(string& buf)
    {
      int total = 0;
      while(total < buf.size())
      {

        int ret = send(_sockfd, buf.c_str() + total, buf.size() - total, 0);
        if(ret < 0)
        {
          perror("send data failed");
          return false;
        }
        total += ret;
      }
      return true;
    }

    //接收数据
    bool Recv(string& buf)
    {
      char tmp[4096] = {0};
      int ret = recv(_sockfd, tmp, 4095, 0);
      if(ret < 0)
      {
        perror("recv error");
        return false;
      }
      else if(ret == 0)
      {
        cout << "connect is broken" << endl;
        return false;
      }
      buf.assign(tmp, ret);
      return true;
    }

    //关闭套接字
    bool Close()
    {
      if(_sockfd != -1)
      {
        close(_sockfd);
        _sockfd = -1;
      }
      return true;
    }

  private:
    int _sockfd;
};

TCP客户端

1.创建套接字   socket()
2.为套接字绑定地址信息(不推荐主动绑定)  bind()
3.向服务端发起连接请求 connect()
4.发送数据  send()
5.接收数据 recv()
6.关闭套接字 close()

#include"tcp.hpp"

int main()
{
  TcpSocket cli_socket;

  //创建套接字
  CHECK(cli_socket.Socket());

  //绑定地址信息(不推荐)

  //向服务器发送连接请求
  CHECK(cli_socket.Connect("192.168.134.141", 9000));

  while(1)
  {
    //发送数据
    string buf;
    cout << "client say:" ;
    getline(cin, buf);
    
    //发送数据失败,直接结束通信
    CHECK(cli_socket.Send(buf));
  
    buf.clear();
    //接收数据
    //接收数据失败,直接结束通信
    CHECK(cli_socket.Recv(buf));
    cout << "serve say:" << buf << endl;
  }
  //关闭套接字
  cli_socket.Close();
  return 0;
}

TCP服务器

1.创建套接字  socket()
2.为套接字绑定地址信息  bind()
3.开始监听 listen()
4.获取新建连接   accept()
5.接收数据  recv()
6.发送数据  send()
7.关闭套接字 close()

单执行流TCP服务器

#include"tcp.hpp"

int main()
{
  TcpSocket lst_socket;
  //创建套接字
  CHECK(lst_socket.Socket());
  //绑定地址信息
  CHECK(lst_socket.Bind("192.168.134.141", 9000));
  //开始监听
  CHECK(lst_socket.Listen());
  while(1)
  {
    //获取新建连接
    TcpSocket newSocket;
    string ip;
    uint16_t port;
    bool ret = lst_socket.Accept(newSocket, &ip, &port);
    if(ret == false)
      continue;
    cout << "new connect  ip:" << ip << "  port:" << port << endl;

    //接收数据
    string buf;
    ret = newSocket.Recv(buf);
    if(ret == false)
    {
      newSocket.Close();
      continue;
    }
    cout << "client say:" << buf << endl;

    buf.clear();
    //发送数据
    cout << "server say:";
    getline(cin, buf);
    ret = newSocket.Send(buf);
    if(ret == false)
    {
      newSocket.Close();
    }
  }
  //关闭套接字
  lst_socket.Close();
  return 0;
}

测试结果:
在这里插入图片描述
根据测试结果来看,当客户端第一次发送数据给服务端时,服务端可以接收数据,并且回复客户端,但是客户端第二次发送数据给服务端时,服务端就收不到客户端的数据,也不能进行回复,这是为什么?

我们先来看下代码中服务端流程:
在这里插入图片描述
accept是阻塞接口,当服务器完成一次接收与发送数据,就会继续到获取新连接接口,此时若没有新的连接,就会一直等待,直到有新的连接到来。所以上面所出现的问题就是因为没有新的连接到来,进入阻塞状态。

recv与send都是阻塞接口,若没有接收到数据或者没有数据发送,就会一直等待,直到能接收到数据或者有数据可以发送。所以,它们三个接口,任意一个接口的调用,都有可能导致服务端流程阻塞。

如果服务端流程阻塞,那么其他客户端也不能进行正常的收发数据,为了解决这个问题,我们提出了多执行流并发处理,为每个客户端都创建一个执行流负责与这个客户端进行通信,这样做的好处就是:

1.主线程卡在获取新建连接这里,但是不影响客户端的通信
2.某个客户端的通信阻塞,也不会影响主线程以及其他线程

实际在代码中的处理:在主线程中,获取新建连接,一旦获取到了则创建一个执行流,通过这个新建连接与客户端进行通信

两种具体的解决方法:多线程、多进程

多线程TCP服务器

#include"tcp.hpp"
#include<pthread.h>

void* pthread_comm(void* newSocket)
{
  TcpSocket* Socket = (TcpSocket*)newSocket;
  while(1)
  {
    bool ret;
    //接收数据
    string buf;
    ret = Socket->Recv(buf);
    if(ret == false)
    {
      Socket->Close();
      delete Socket;
      return NULL;
    }
    cout << "client say:" << buf << endl;

    buf.clear();
    //发送数据
    cout << "server say:";
    getline(cin, buf);
    ret = Socket->Send(buf);
    if(ret == false)
    {
      Socket->Close();
      delete Socket;
      return NULL;
    }
  }
  Socket->Close();
  delete Socket;
  return NULL;
}

int main()
{
  TcpSocket lst_socket;
  //创建套接字
  CHECK(lst_socket.Socket());
  //绑定地址信息
  CHECK(lst_socket.Bind("192.168.134.141", 9000));
  //开始监听
  CHECK(lst_socket.Listen());
  while(1)
  {
    //获取新建连接
    TcpSocket* newSocket = new TcpSocket;
    string ip;
    uint16_t port;
    bool ret = lst_socket.Accept(*newSocket, &ip, &port);
    if(ret == false)
      continue;
    cout << "new connect-->ip:" << ip << " port:" << port << endl;

    //获取新建连接之后,创建线程负责与客户端进行通信
    pthread_t tid;
    int res = pthread_create(&tid, NULL, pthread_comm, (void*)newSocket);
    if(res != 0)
    {
      perror("create pthread failed");
      continue;
    }
    //线程分离
    pthread_detach(tid);
  }
  //关闭套接字
  lst_socket.Close();
  return 0;
}

注意:

普通线程与主线程数据共享,主线程不能随意释放套接字,一旦释放其他线程无法使用。

测试:
在这里插入图片描述

多进程TCP服务器

#include"tcp.hpp"
#include<stdlib.h>
#include<signal.h>

void work(TcpSocket& newSocket)
{
  //接收数据
  while(1)
  {
    bool ret;
    string buf;
    ret = newSocket.Recv(buf);
    if(ret == false)
    {
     //获取数据失败,关闭通信套接字,退出子进程 
      newSocket.Close();
      exit(0);
    }
    cout << "client say:" << buf << endl;

    buf.clear();
    //发送数据
    cout << "server say:";
    getline(cin, buf);
    ret = newSocket.Send(buf);
    if(ret == false)
    {
      // 发送数据失败,关闭通信套接字,退出子进程
      newSocket.Close();
      exit(0);
    }
  }
  newSocket.Close();
  return;
}

int main()
{
  signal(SIGCHLD, SIG_IGN);//信号函数,如果有子进程退出,忽略它的退出原因
  TcpSocket lst_socket;
  //创建套接字
  CHECK(lst_socket.Socket());
  //绑定地址信息
  CHECK(lst_socket.Bind("192.168.134.141", 9000));
  //开始监听
  CHECK(lst_socket.Listen());
  while(1)
  {
    //获取新建连接
    TcpSocket newSocket;
    string ip;
    uint16_t port;
    bool ret = lst_socket.Accept(newSocket, &ip, &port);
    if(ret == false)
      continue;
    cout << "new connect  ip:" << ip << "  port:" << port << endl;

    //创建子进程,子进程与客户端进行通信
    pid_t pid = fork();
    if(pid < 0)
    {
      perror("create fork failed");
      //创建子进程失败,关闭的是父进程的通信套接字
      newSocket.Close();
      continue;
    }
    else if(pid == 0) //子进程
    {
      work(newSocket);
    }
    //父进程
    //子进程用来通信,父进程的通信套接字就可以关闭了,防止资源泄漏
    //父子进程数据独有,父进程关闭不会对子进程造成影响
    newSocket.Close();
  }
  //关闭套接字
  lst_socket.Close();
  return 0;
}

注意:

父子进程数据各自独有,父进程用不到新建套接字因此创建子进程之后直接释放掉,否则会造成资源泄漏。同时也要注意僵尸进程的问题,我们在这里采用信号的方式,一旦有子进程退出的信号,我们就忽略处理,不去在意子进程的退出原因。

测试:
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值