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

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

简单的TCP网络程序


服务端


服务端创建套接字


将TCP服务器封装成一个类,当我们定义出一个服务器对象后需要马上对服务器进行初始化,而初始化TCP服务器要做的第一件事就是创建套接字

TCP服务器在调用socket函数创建套接字时,参数设置如下:

  • 协议家族选择AF_INET,这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址。
  • 创建套接字时所需的服务类型应该是SOCK_STREAM,这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
  • 协议类型默认设置为0即可。

如果创建套接字后获得的文件描述符是小于0的,说明套接字创建失败,此时也就没必要进行后续操作了,直接终止程序即可。

class TcpServer
{
public:
  void InitServer()
  {
    //创建套接字
    _sock = socket(AF_INET, SOCK_STREAM, 0);
    if (_sock < 0){
      std::cerr << "socket error" << std::endl;
      exit(2);
    }
  }
  ~TcpServer()
  {
    if (_sock >= 0){
      close(_sock);
    }
  }
private:
  int _sock; //套接字
};

注意:

  • 实际TCP服务器创建套接字的做法与UDP服务器是一样的,只不过创建套接字时TCP需要的是流式服务,而UDP需要的是用户数据报服务。
  • 当析构服务器时,可以将服务器对应的文件描述符进行关闭。

服务端绑定


套接字创建完毕后我们实际只是在系统层面上打开了一个文件,该文件还没有与网络关联起来,因此创建完套接字后我们还需要调用bind函数进行绑定操作。

绑定步骤如下:

  • 定义一个struct sockaddr_in结构体,将服务器网络相关的属性信息填充到该结构体当中,比如协议家族,IP地址,端口号等。
  • 填充服务器网络相关的属性信息时,协议家族对应就是AF_INET,端口号就是当前TCP服务器程序的端口号。在设置端口号时,需要调用htos函数将端口号由主机序列转为网络序列。
  • 在设置服务器的IP地址时,我们可以设置为本地环回127.0.0.1,表示本地通信。也可以设置为公网IP地址,表示网络通信。
  • 如果使用的是云服务器,那么在设置服务器的IP地址时,不需要显示绑定IP地址,直接将IP地址设置为INADDR_ANY即可,此时服务器就可以从本地任何一张网卡当中读取数据。此外,由于INADDE_ANY本质就是0,因此在设置时不需要进行网络字节序的转换。
  • 填充完故武器网络相关的属性信息后,需要调用bind函数进行绑定。绑定实际就是将文件与网络相关联起来,如果绑定失败也没必要进行后续操作了,直接终止程序即可。

由于TCP服务器初始化时需要服务器的端口号,因此在服务器类当中需要引入端口号,当实例化服务器对象时就需要给传入一个端口号。而由于我当前使用的是云服务器,因此在绑定TCP服务器的IP地址时不需要绑定公网IP地址,直接绑定INADDR_ANY即可,因此我这里没有在服务器类当中引入IP地址。

class TcpServer
{
public:
  TcpServer(int port)
    : _sock(-1)
    , _port(port)
  {}
  void InitServer()
  {
    //创建套接字
    _sock = socket(AF_INET, SOCK_STREAM, 0);
    if (_sock < 0){
      std::cerr << "socket error" << std::endl;
      exit(2);
    }
    //绑定
    struct sockaddr_in local;
    memset(&local, '\0', sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(_port);
    local.sin_addr.s_addr = INADDR_ANY;
    if (bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
      std::cerr << "bind error" << std::endl;
      exit(3);
    }
  }
  ~TcpServer()
  {
    if (_sock >= 0){
      close(_sock);
    }
  }
private:
  int _sock; //监听套接字
  int _port; //端口号
};

注意:TCP服务器绑定步骤和UDP服务器完全一样,没有区别。

服务端监听


UDP服务器的初始化操作只有两步,第一步就是创建套接字,第二部就是绑定。而TCP服务器是面向连接的,客户端在正式向TCP服务器发送数据之前,需要先与TCP服务器建立连接,然后才能与服务器进行通信。

因此TCP服务器需要时刻注意是否有客户端发来连接请求,此时就需要将TCP服务器创建的套接字设置为监听状态。

listen函数

设置套接字为监听状态的函数叫做listen,该函数的函数原型如下:

int listen(int sockfd, int backlog);

参数说明:

  • sockfd:需要设置为监听状态的套接字对应的文件描述符.
  • backlog:全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度,一般不要设置太大,设置为5或10即可。

返回值说明:

  • 监听成功返回0,监听失败返回-1,同时错误码会被设置。
  • 所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。

请求队列

当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。

缓冲区的长度(能存放多少个客户端请求)可以通过 listen() 函数的 backlog 参数指定,但究竟为多少并没有什么标准,可以根据你的需求来定,并发量小的话可以是10或者20。


如果将 backlog 的值设置为 SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。

当请求队列满时,就不再接收新的请求,对于 Linux,客户端会收到 ECONNREFUSED 错误。

#define BACKLOG 5
class TcpServer
{
public:
  void InitServer()
  {
    //创建套接字
    _listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (_listen_sock < 0){
      std::cerr << "socket error" << std::endl;
      exit(2);
    }
    //绑定
    struct sockaddr_in local;
    memset(&local, '\0', sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(_port);
    local.sin_addr.s_addr = INADDR_ANY;
    if (bind(_listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
      std::cerr << "bind error" << std::endl;
      exit(3);
    }
    //监听
    if (listen(_listen_sock, BACKLOG) < 0){
      std::cerr << "listen error" << std::endl;
      exit(4);
    }
  }
private:
  int _listen_sock; //监听套接字
  int _port; //端口号
};

注意:listen() 只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept() 函数。

accept() 函数。

服务端获取连接


TCP服务器初始化后就可以开始运行了,但TCP服务器在与客户端进行网络通信之前,服务器需要先获取到客户端的连接请求。

当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。

#include <sys/types.h>    
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockdf:
  socket文件描述符
addr:
  传出参数,返回链接客户端地址信息,含IP地址和端口号
addrlen:
  传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
返回值:
  成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno

accept函数返回的套接字是什么?

调用accept函数获取连接时,是从监听套接字当中获取的。如果accept函数获取连接成功,此时会返回接收到的套接字对应的文件描述符。

监听套接字与accept函数返回的套接字的作用:

  • 监听套接字:用于获取客户端发来的连接请求。accept函数会不断从监听套接字当中获取新连接。
  • accept函数返回的套接字:用于为本次accept获取到的连接提供服务。监听套接字的任务只是不断获取新连接,而真正为这些连接提供服务的套接字是accept函数返回的套接字,而不是监听套接字。
  • 最后需要说明的是:listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,

listen() 后面的代码会继续执行,直到遇到 accept()。accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。。

服务端获取连接

服务端在获取连接时需要注意:

  • accept函数获取连接时可能会失败,但TCP服务器不会因为获取某个连接失败而退出,因此服务端获取连接失败后应该继续获取连接。
  • 如果要将获取到的连接对应客户端的IP地址和端口号信息进行输出,需要调用inet_ntoa函数将整数IP转换成字符串IP,调用ntohs函数将端口号由网络序列转换成主机序列。
  • inet_ntoa函数在底层实际做了两个工作,一是将网络序列转换成主机序列,二是将主机序列的整数IP
class TcpServer
{
public:
  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->"<<sock<<" ["<<client_ip<<"]:"<<client_port<<std::endl;
    }
  }
private:
  int _listen_sock; //监听套接字
  int _port; //端口号
};

服务端接收连接测试

现在我们可以做一下简单的测试,看看当前服务器能否成功接收请求连接。在运行服务端时需要传入一个端口号作为服务端的端口号,然后我们用该端口号构造一个服务端对象,对服务端进行初始化后启动服务端即可。

void Usage(std::string proc)
{
  std::cout << "Usage: " << proc << " port" << std::endl;
}
int main(int argc, char* argv[])
{
  if (argc != 2){
    Usage(argv[0]);
    exit(1);
  }
  int port = atoi(argv[1]);
  TcpServer* svr = new TcpServer(port);
  svr->InitServer();
  svr->Start();
  return 0;
}

运行服务端

image.png

服务端运行后,通过netstat命令可以查看到一个程序名为tcp_server的服务程序,它绑定的端口就是8081,而由于服务器绑定的是INADDR_ANY,因此该服务器的本地IP地址是0.0.0.0,这就意味着该TCP服务器可以读取本地任何一张网卡里面的数据。此外,最重要的是当前该服务器所处的状态是LISTEN状态,表明当前服务器可以接收外部的请求连接。

image.png

使用telnet命令连接当前TCP服务器后可以看到,此时服务器接收到了一个连接,为该连接提供服务的套接字对应的文件描述符就是4。因为0、1、2是默认打开的,其分别对应标准输入流、标准输出流和标准错误流,而3号文件描述符在初始化服务器时分配给了监听套接字,因此当第一个客户端发起连接请求时,为该客户端提供服务的套接字对应的文件描述符就是4。

image.png

相关文章
|
8天前
|
安全 Java 数据处理
Python网络编程基础(Socket编程)多线程/多进程服务器编程
【4月更文挑战第11天】在网络编程中,随着客户端数量的增加,服务器的处理能力成为了一个重要的考量因素。为了处理多个客户端的并发请求,我们通常需要采用多线程或多进程的方式。在本章中,我们将探讨多线程/多进程服务器编程的概念,并通过一个多线程服务器的示例来演示其实现。
|
29天前
|
移动开发 网络协议 安全
网络面试题:什么是 TCP/IP?
网络面试题:什么是 TCP/IP?
43 0
网络面试题:什么是 TCP/IP?
|
30天前
|
监控 负载均衡 网络协议
TCP重传与超时机制:解锁网络性能之秘
TCP重传与超时机制:解锁网络性能之秘
58 0
|
2天前
|
网络协议 Java API
深度剖析:Java网络编程中的TCP/IP与HTTP协议实践
【4月更文挑战第17天】Java网络编程重在TCP/IP和HTTP协议的应用。TCP提供可靠数据传输,通过Socket和ServerSocket实现;HTTP用于Web服务,常借助HttpURLConnection或Apache HttpClient。两者结合,构成网络服务基础。Java有多种高级API和框架(如Netty、Spring Boot)简化开发,助力高效、高并发的网络通信。
|
4天前
|
SQL 安全 Java
Java安全编程:防范网络攻击与漏洞
【4月更文挑战第15天】本文强调了Java安全编程的重要性,包括提高系统安全性、降低维护成本和提升用户体验。针对网络攻击和漏洞,提出了防范措施:使用PreparedStatement防SQL注入,过滤和转义用户输入抵御XSS攻击,添加令牌对抗CSRF,限制文件上传类型和大小以防止恶意文件,避免原生序列化并确保数据完整性。及时更新和修复漏洞是关键。程序员应遵循安全编程规范,保障系统安全。
|
11天前
|
网络协议 安全 网络性能优化
|
21天前
|
缓存 网络协议 数据库连接
【底层服务/编程功底系列】「网络通信体系」深入探索和分析TCP协议的运输连接管理的核心原理和技术要点
【底层服务/编程功底系列】「网络通信体系」深入探索和分析TCP协议的运输连接管理的核心原理和技术要点
20 0
|
30天前
|
网络协议 算法 Linux
探索TCP状态机之旅:发现网络连接的生命周期与神秘魅力
探索TCP状态机之旅:发现网络连接的生命周期与神秘魅力
67 0
|
1月前
|
机器学习/深度学习 数据采集 人工智能
m基于深度学习网络的手势识别系统matlab仿真,包含GUI界面
m基于深度学习网络的手势识别系统matlab仿真,包含GUI界面
38 0
|
1月前
|
机器学习/深度学习 算法 计算机视觉
基于yolov2深度学习网络的火焰烟雾检测系统matlab仿真
基于yolov2深度学习网络的火焰烟雾检测系统matlab仿真