【网络篇】第七篇——网络套接字编程(三)(TCP详解)(二)

简介: 【网络篇】第七篇——网络套接字编程(三)(TCP详解)

服务端处理请求


现在TCP服务器已经能够获取连接请求了,下面当然就是要对获取到的连接进行处理。但此时为客户端提供服务的不是监听套接字,因为监听套接字获取到一个连接后会继续获取下一个请求连接,为对应客户端提供服务的套接字实际是accept函数返回的套接字,下面就将其称为“服务套接字”。

为了让通信双方都能看到对应的现象,我们这里就实现一个简单的回声TCP服务器,服务端在为客户端提供服务时就简单的将客户端发来的数据进行输出,并且将客户端发来的数据重新发回给客户端即可。当客户端拿到服务端的响应数据后再将该数据进行打印输出,此时就能确保服务端和客户端能够正常通信了。

read函数

TCP服务器读取数据的函数叫做read,该函数的函数原型如下:

ssize_t read(int fd, void *buf, size_t count);

参数说明:

  • fd:特定的文件描述符,表示从该文件描述符中读取数据。
  • buf:数据的存储位置,表示将读取到的数据存储到该位置。
  • count:数据的个数,表示从该文件描述符中读取数据的字节数。

返回值说明:

  • 如果返回值大于0,则表示本次实际读取到的字节个数。
  • 如果返回值等于0,则表示对端已经把连接关闭了。
  • 如果返回值小于0,则表示读取时遇到了错误。

read返回值为0表示对端连接关闭

这实际和本地进程间通信中的管道通信是类似的,当使用管道进行通信时,可能会出现如下情况:

写端进程不写,读端进程一直读,此时读端进程就会被挂起,因为此时数据没有就绪。

读端进程不读,写端进程一直写,此时当管道被写满后写端进程就会被挂起,因为此时空间没有就绪。

写端进程将数据写完后将写端关闭,此时当读端进程将管道当中的数据读完后就会读到0。

读端进程将读端关闭,此时写端进程就会被操作系统杀掉,因为此时写端进程写入的数据不会被读取。

这里的写端就对应客户端,如果客户端将连接关闭了,那么此时服务端将套接字当中的信息读完后就会读取到0,因此如果服务端调用read函数后得到的返回值为0,此时服务端就不必再为该客户端提供服务了。

write函数

TCP服务器写入数据的函数叫做write,该函数的函数原型如下:

ssize_t write(int fd, const void *buf, size_t count);

参数说明:

  • fd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
  • buf:需要写入的数据。
  • count:需要写入数据的字节个数。
  • 返回值说明:
  • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

当服务端调用read函数收到客户端的数据后,就可以再调用write函数将该数据再响应给客户端。

服务端处理请求

需要注意的是,服务端读取数据是服务套接字中读取的,而写入数据的时候也是写入进服务套接字的。也就是说这里为客户端提供服务的套接字,既可以读取数据也可以写入数据,这就是TCP全双工的通信的体现。


在从服务套接字中读取客户端发来的数据时,如果调用read函数后得到的返回值为0,或者读取出错了,此时就应该直接将服务套接字对应的文件描述符关闭。因为文件描述符本质就是数组的下标,因此文件描述符的资源是有限的,如果我们一直占用,那么可用的文件描述符就会越来越少,因此服务完客户端后要及时关闭对应的文件描述符,否则会导致文件描述符泄漏

class TcpServer
{
public:
  void Service(int sock, std::string client_ip, int client_port)
  {
    char buffer[1024];
    while (true){
      ssize_t size = read(sock, buffer, sizeof(buffer)-1);
      if (size > 0){ //读取成功
        buffer[size] = '\0';
        std::cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << std::endl;
        write(sock, buffer, size);
      }
      else if (size == 0){ //对端关闭连接
        std::cout << client_ip << ":" << client_port << " close!" << std::endl;
        break;
      }
      else{ //读取失败
        std::cerr << sock << " read error!" << std::endl;
        break;
      }
    }
    close(sock); //归还文件描述符
    std::cout << client_ip << ":" << client_port << " service done!" << std::endl;
  }
  void Start()
  {
    for (;;){
      //获取连接
      struct sockaddr_in peer;
      memset(&peer, '\0', sizeof(peer));
      socklen_t len = sizeof(peer);
      int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
      if (sock < 0){
        std::cerr << "accept error, continue next" << std::endl;
        continue;
      }
      std::string client_ip = inet_ntoa(peer.sin_addr);
      int client_port = ntohs(peer.sin_port);
      std::cout << "get a new link [" << client_ip << "]:" << client_port << std::endl;
      //处理请求
      Service(sock, client_ip, client_port);
    }
  }
private:
  int _listen_sock; //监听套接字
  int _port; //端口号
};

客户端


客户端创建套接字


同样的,我们将客户端也封装成一个类,当我们定义出一个客户端对象后也需要对其进行初始化,而初始化客户端唯一需要做的就是创建套接字。而客户端在调用socket函数创建套接字时,参数设置与服务端创建套接字时是一样的。

客户端不需要进行绑定和监听:

  • 服务端要进行绑定是因为服务端的IP地址和端口号必须要众所周知,不能随意改变。而客户端虽然也需要IP地址和端口号,但是客户端并不需要我们进行绑定操作,客户端连接服务器时系统会自动指定一个端口号给客户端。
  • 服务端需要进行监听是因为服务端需要通过监听来获取新连接,但是不会有人主动连接客户端,因此客户端是不需要进行监听操作的。

此外,客户端必须要知道它连接的服务端的IP地址和端口号,因此客户端除了要有自己的套接字之外,还需要知道服务端的IP地址和端口号,这样客户端才能够通过套接字向指定服务器进行通信。

class TcpClient
{
public:
  TcpClient(std::string server_ip, int server_port)
    : _sock(-1)
    , _server_ip(server_ip)
    , _server_port(server_port)
  {}
  void InitClient()
  {
    //创建套接字
    _sock = socket(AF_INET, SOCK_STREAM, 0);
    if (_sock < 0){
      std::cerr << "socket error" << std::endl;
      exit(2);
    }
  }
  ~TcpClient()
  {
    if (_sock >= 0){
      close(_sock);
    }
  }
private:
  int _sock; //套接字
  std::string _server_ip; //服务端IP地址
  int _server_port; //服务端端口号
};

客户端连接服务器


由于客户端不需要绑定,也不需要监听,因此当客户端创建完套接字后就可以向服务端发起连接请求。

connect函数

发起连接请求的函数叫做connect,该函数的函数原型如下:

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:

  • sockfd:特定的套接字,表示通过该套接字发起连接请求。
  • addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:传入的addr结构体的长度。

返回值说明:

  • 连接或绑定成功返回0,连接失败返回-1,同时错误码会被设置。

客户端连接服务器

需要注意的是,客户端不是不需要进行绑定,而是不需要我们自己进行绑定操作,当客户端向服务端发起连接请求时,系统会给客户端随机指定一个端口号进行绑定。因为通信双方都必须要有IP地址和端口号,否则无法唯一标识通信双方。也就是说,如果connect函数调用成功了,客户端本地会随机给该客户端绑定一个端口号发送给对端服务器。


此外,调用connect函数向服务端发起连接请求时,需要传入服务端对应的网络信息,否则connect函数也不知道该客户端到底是要向哪一个服务端发起连接请求。

class TcpClient
{
public:
  void Start()
  {
    struct sockaddr_in peer;
    memset(&peer, '\0', sizeof(peer));
    peer.sin_family = AF_INET;
    peer.sin_port = htons(_server_port);
    peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());
    if (connect(_sock, (struct sockaddr*)&peer, sizeof(peer)) == 0){ //connect success
      std::cout << "connect success..." << std::endl;
      Request(); //发起请求
    }
    else{ //connect error
      std::cerr << "connect failed..." << std::endl;
      exit(3);
    }
  }
private:
  int _sock; //套接字
  std::string _server_ip; //服务端IP地址
  int _server_port; //服务端端口号
};

客户端发起请求


由于我们实现的是一个简单的回声服务器,因此当客户端连接到服务端后,客户端就可以向服务端发送数据了,这里我们可以让客户端将用户输入的数据发送给服务端,发送时调用write函数向套接字当中写入数据即可。

当客户端将数据发送给服务端后,由于服务端读取到数据后还会进行回显,因此客户端在发送数据后还需要调用read函数读取服务端的响应数据,然后将该响应数据进行打印,以确定双方通信无误。

class TcpClient
{
public:
  void Request()
  {
    std::string msg;
    char buffer[1024];
    while (true){
      std::cout << "Please Enter# ";
      getline(std::cin, msg);
      write(_sock, msg.c_str(), msg.size());
      ssize_t size = read(_sock, buffer, sizeof(buffer)-1);
      if (size > 0){
        buffer[size] = '\0';
        std::cout << "server echo# " << buffer << std::endl;
      }
      else if (size == 0){
        std::cout << "server close!" << std::endl;
        break;
      }
      else{
        std::cerr << "read error!" << std::endl;
        break;
      }
    }
  }
  void Start()
  {
    struct sockaddr_in peer;
    memset(&peer, '\0', sizeof(peer));
    peer.sin_family = AF_INET;
    peer.sin_port = htons(_server_port);
    peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());
    if (connect(_sock, (struct sockaddr*)&peer, sizeof(peer)) == 0){ //connect success
      std::cout << "connect success..." << std::endl;
      Request(); //发起请求
    }
    else{ //connect error
      std::cerr << "connect failed..." << std::endl;
      exit(3);
    }
  }
private:
  int _sock; //套接字
  std::string _server_ip; //服务端IP地址
  int _server_port; //服务端端口号
};

在运行客户端程序时我们就需要携带上服务端对应的IP地址和端口号,然后我们就可以通过服务端的IP地址和端口号构造出一个客户端对象,对客户端进行初始后启动客户端即可。

void Usage(std::string proc)
{
  std::cout << "Usage: " << proc << "server_ip server_port" << std::endl;
}
int main(int argc, char* argv[])
{
  if (argc != 3){
    Usage(argv[0]);
    exit(1);
  }
  std::string server_ip = argv[1];
  int server_port = atoi(argv[2]);
  TcpClient* clt = new TcpClient(server_ip, server_port);
  clt->InitClient();
  clt->Start();
  return 0;
}

服务器测试


现在服务端和客户端均已编写完毕,下面我们进行测试。测试时我们先启动服务端,然后通过netstat命令进行查看,此时我们就能看到一个名为tcp_server的服务进程,该进程当前处于监听状态。

image.png

然后再通过./tcp_client IP地址 端口号的形式运行客户端,此时客户端就会向服务端发起连接请求,服务端获取到请求后就会为该客户端提供服务。


当客户端向服务端发送消息后,服务端可以通过打印的IP地址和端口号识别出对应的客户端,而客户端也可以通过服务端响应回来的消息来判断服务端是否收到了自己发送的消息。


如果此时客户端退出了,那么服务端在调用read函数时得到的返回值就是0,此时服务端也就知道客户端退出了,进而会终止对该客户端的服务。

image.png

注意: 此时是服务端对该客户端的服务终止了,而不是服务器终止了,此时服务器依旧在运行,它在等待下一个客户端的连接请求。

单执行流服务器的弊端


当我们仅用一个客户端连接服务端时,这一个客户端能够正常享受到服务端的服务。

image.png

但在这个客户端正在享受服务端的服务时,我们让另一个客户端也连接服务器,此时虽然在客户端显示连接是成功的,但这个客户端发送给服务端的消息既没有在服务端进行打印,服务端也没有将该数据回显给该客户端。

只有当第一个客户端退出后,服务端才会将第二个客户端发来是数据进行打印,并回显该第二个客户端。

单执行流的服务器

通过实现现象可以看到,这服务端只有服务完一个客户端后才会服务另一个客户端,因为我们目前所写的是一个单执行流版的服务器,这个服务器依次只能为一个客户端提供服务。

当服务端调用accept函数获取到连接后就给该客户端提供服务,但在服务端提供服务期间可能会有其他客户端发起连接请求,但由于当前服务器是单执行流的,只能服务完当前客户端后才能继续服务下一个客户端。

客户端为什么会显示连接成功?

当服务端在给第一个客户端提供服务期间,第二个客户端向服务端发起的连接请求时是成功的,只不过服务端没有调用accept函数将该连接获取上来罢了。


实际在底层会为我们维护一个连接队列,服务端没有accept的新连接就会放到这个连接队列当中,而这个连接队列的最大长度就是通过listen函数的第二个参数来指定的,因此服务端虽然没有获取第二个客户端发来的连接请求,但是在第二个客户端那里显示是连接成功

————————————————

版权声明:本文为CSDN博主「接受平凡 努力出众」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/m0_58367586/article/details/127583190的。

如何解决?

单执行流的服务器一次只能给一个客户端提供服务,此时服务器的资源并没有得到充分利用,因此服务器一般是不会写成单执行流的。要解决这个问题就需要将服务器改为多执行流的,此时就要引入多进程或多线程。

相关文章
|
20天前
|
机器学习/深度学习 API Python
Python 高级编程与实战:深入理解网络编程与异步IO
在前几篇文章中,我们探讨了 Python 的基础语法、面向对象编程、函数式编程、元编程、性能优化、调试技巧、数据科学、机器学习、Web 开发和 API 设计。本文将深入探讨 Python 在网络编程和异步IO中的应用,并通过实战项目帮助你掌握这些技术。
|
1月前
|
网络协议 测试技术 Linux
Golang 实现轻量、快速的基于 Reactor 模式的非阻塞 TCP 网络库
gev 是一个基于 epoll 和 kqueue 实现的高性能事件循环库,适用于 Linux 和 macOS(Windows 暂不支持)。它支持多核多线程、动态扩容的 Ring Buffer 读写缓冲区、异步读写和 SO_REUSEPORT 端口重用。gev 使用少量 goroutine,监听连接并处理读写事件。性能测试显示其在不同配置下表现优异。安装命令:`go get -u github.com/Allenxuxu/gev`。
|
3月前
|
负载均衡 网络协议 算法
不为人知的网络编程(十九):能Ping通,TCP就一定能连接和通信吗?
这网络层就像搭积木一样,上层协议都是基于下层协议搭出来的。不管是ping(用了ICMP协议)还是tcp本质上都是基于网络层IP协议的数据包,而到了物理层,都是二进制01串,都走网卡发出去了。 如果网络环境没发生变化,目的地又一样,那按道理说他们走的网络路径应该是一样的,什么情况下会不同呢? 我们就从路由这个话题聊起吧。
99 4
不为人知的网络编程(十九):能Ping通,TCP就一定能连接和通信吗?
|
4月前
|
监控 安全
公司上网监控:Mercury 在网络监控高级逻辑编程中的应用
在数字化办公环境中,公司对员工上网行为的监控至关重要。Mercury 作为一种强大的编程工具,展示了在公司上网监控领域的独特优势。本文介绍了使用 Mercury 实现网络连接监听、数据解析和日志记录的功能,帮助公司确保信息安全和工作效率。
138 51
|
3月前
|
网络协议
TCP报文格式全解析:网络小白变高手的必读指南
本文深入解析TCP报文格式,涵盖源端口、目的端口、序号、确认序号、首部长度、标志字段、窗口大小、检验和、紧急指针及选项字段。每个字段的作用和意义详尽说明,帮助理解TCP协议如何确保可靠的数据传输,是互联网通信的基石。通过学习这些内容,读者可以更好地掌握TCP的工作原理及其在网络中的应用。
|
4月前
|
网络协议
网络通信的基石:TCP/IP协议栈的层次结构解析
在现代网络通信中,TCP/IP协议栈是构建互联网的基础。它定义了数据如何在网络中传输,以及如何确保数据的完整性和可靠性。本文将深入探讨TCP/IP协议栈的层次结构,揭示每一层的功能和重要性。
171 5
|
4月前
|
监控 网络协议 网络性能优化
网络通信的核心选择:TCP与UDP协议深度解析
在网络通信领域,TCP(传输控制协议)和UDP(用户数据报协议)是两种基础且截然不同的传输层协议。它们各自的特点和适用场景对于网络工程师和开发者来说至关重要。本文将深入探讨TCP和UDP的核心区别,并分析它们在实际应用中的选择依据。
134 3
|
4月前
|
数据库连接 Go 数据库
Go语言中的错误注入与防御编程。错误注入通过模拟网络故障、数据库错误等,测试系统稳定性
本文探讨了Go语言中的错误注入与防御编程。错误注入通过模拟网络故障、数据库错误等,测试系统稳定性;防御编程则强调在编码时考虑各种错误情况,确保程序健壮性。文章详细介绍了这两种技术在Go语言中的实现方法及其重要性,旨在提升软件质量和可靠性。
78 1
|
4月前
|
网络协议 网络安全 网络虚拟化
本文介绍了十个重要的网络技术术语,包括IP地址、子网掩码、域名系统(DNS)、防火墙、虚拟专用网络(VPN)、路由器、交换机、超文本传输协议(HTTP)、传输控制协议/网际协议(TCP/IP)和云计算
本文介绍了十个重要的网络技术术语,包括IP地址、子网掩码、域名系统(DNS)、防火墙、虚拟专用网络(VPN)、路由器、交换机、超文本传输协议(HTTP)、传输控制协议/网际协议(TCP/IP)和云计算。通过这些术语的详细解释,帮助读者更好地理解和应用网络技术,应对数字化时代的挑战和机遇。
256 3
|
4月前
|
网络协议 安全 Go
Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
【10月更文挑战第28天】Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
105 13

热门文章

最新文章