写在前面

下面我们开始正式进入网络编程的代码认识,今天我们先用这些协议,后面我们再说他们的原理.我们这里最重要的是认识几个接口,我们通过一个一些小的代码来认识他们.

知识储备

在使用协议之前,我们这里先来储备一些知识,为了后面的更好的使用.

IP地址

这个我们已经谈过了,就是源IP和目的IP.他们分别标定两台主机,这两台主机不一定非要在一个网段中.

端口号

总不所有的信息都放在一台主机一个地方,我们把主机分为多个端口,我们用IP来标识主机,我们使用网络通信可不是两台主机进行通信,这是不合理的,主机交互是没有意义的.网络通信本质是用户和用户进行交互,就像抖音一样 ,用户不是机器,用户的身份是用程序实现的,我们是用客户端来完成的,就像我们手机有很多软件,程序在运行中就是进程,网络通信的本质就是进程之间的通信.IP地址可以完成主机和主机的通信,主机的进程才是发送和接受信息的一方.

所以IP地址标定主机的唯一性,那么我们用什么标志进程的唯一性呢?这里是端口号(port),也就是IP:PORT可以标定互联网唯一的一个进程.我们把IP:PORT称之为socket,也就是插座.进程是具有独立性的,一个进程挂了就不影响,一台主机都是如此独立,两台更是如此.那么进程想要通信,必须要先看到同一份资源,这里同一份资源就是网络.

  • 端口号是 2字节 16位的整数
  • 端口号用来标志一个进程
  • 一个端口号只能被一个进程占用,但是多个端口号可以指向同一个进程

那么进程pid和端口号有什么区别?用pid不是也是可以知道一个进程吗?可以的.但是进程pid是进程管理概念,端口号是网络概念,如果我们使用pid也是可以的,技术上是支持的,但是一个值作为两种用途,但是这里强耦合了,而且不是所有的进程都会在网络中通信,端口号更像一种证明,证明该进程是支持网络通信的,这就像身份证号和学号 ,身份证号是可以标志人的唯一性,但是学校会给我们一个学号,这个学号代表这个人是我们学校的学生.

那么我想问是如何把端口号和pid进行进行对应呢?这里OS内核里面是给我们准备了哈希表的.这里有两张著名的哈希表,例如kill -9 ,可以很容易找到对应的进程,这里还有一张端口号对应的哈希表.我们想问,一个端口号只能被一个进程占用,那么一个进程可以绑定多个端口号吗? 可以的, 为什么? 客户端A在进行通信时,也可能和A的子模块在通信,我们只需要保证借助端口号找到一个确定的进程就可以了.

字节序

我们知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出.
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存.
  • 网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.

此时我们就可以规定了,我们所有在网络上传输的数据一律按照大端进行传输.此时我们就疑惑了我们规定小端不可以吗?首先是可以的,但是比较麻烦,我们想一想.我们发送数是从低地址开始的,拿的也是从地址开始的,我们希望第一个发送到的是高权值位的数据,这样我们后面闹拿到的数据可以往后面进行很好的拼接,此时大段发送的话非常容易拼接,如果我们设置为小端发送,此时接收方拿到数据还需要进行字节序的转变,效率有点低.

那么我想问,我不知道自己的电脑是大端机还是小端机,不用关心,这里有内置的函数可以让我们的数据直接转换为大端,这样我们就不用考虑这么多了.

#include <arpa/inet.h>
// 主机转网络 -- 32比特
uint32_t htonl(uint32_t hostlong);

uint16_t htons(uint16_t hostshort);

uint32_t ntohl(uint32_t netlong);
// 网络转主机 -- 16比特
uint16_t ntohs(uint16_t netshort);

传输层协议

下面我们要认识传输层的两个协议,这里简单认识一下,我们直接用起来,先把用法搞定,后面我们再说原理.

  • UDP协议
  • TCP协议

UDP协议

此处我们是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识,后面再详细讨论

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

TCP协议

此处我们也先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题.

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

注意,我们上面说的可靠传输和不可靠传输是协议的一种特性,不带有一点的主观色彩,可靠意味着我们代码做的更多,更见复杂,那么效率也就有可能低了.

UDP协议

下面我们说一下我们UDP协议的使用,很简单.我们创建一个

socket API

先来认识一下UDP协议的里面的接口,这是我们编程的基础.

socket

我们先来这个函数,使用是非常简单的,这就像我们在网络中打开一个文件描述符

#include <sys/types.h>        
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

万字解析网络套接字_#include

先来认识一下参数,都是非常简单的,重点关注我们的返回值是一个文件描述符.

int main()
{
  int fd = socket(AF_INET, SOCK_DGRAM, 0);
  cout << fd << endl;
  return 0;
}

万字解析网络套接字_服务器_02

bind

既然我们的网络套接字已经创建好了,那么现在我们应该让服务端绑定我们的主机信息了.这里认识几个结构体,结构体的作用就是保存我们的信心.

万字解析网络套接字_网络_03

这里说下这三个结构体,其中struct sockaddr是我们作为参数要进行传递的,后面的参数我们都是指针,指针通过访问16位地址类型来进行判断是后面哪一个结构类型,其中struct sockaddr_in是网络通信,也就是域间通信,struct sockaddr_un是本地通信,他们两个的16位地址类型就是说的我们上面创建网络套接字说的宏.

先来看一下我们的struct sockaddr_in结构体的属性.

struct sockaddr_in
{
  __SOCKADDR_COMMON(sin_); // 宏
  in_port_t sin_port;      // 端口号
  struct in_addr sin_addr; // 这也是一个结构体,保存了IP地址

  unsigned char sin_zero[sizeof(struct sockaddr)
                             __SOCKADDR_COMMON_SIZE -
                         sizeof(in_port_t) -
                         sizeof(struct in_addr)];
};

typedef uint32_t in_addr_t;
struct in_addr
{
  in_addr_t s_addr; // 这里就是IP地址类型
};

上面我们已经知道了我们的端口号是需要进行大端保存的,请问我们的IP地址需要保存吗?如果需要请问是如何保存的?

这里很简单,我们是需要进行保存到,但是我们知道IP地址是一个4字节, 32位的整数,平常我们也用点分十进制的字符串风格的方式进行表示,这就意味这我们要把字符串转化为二进制形式,和二进制形式转化为字符串.先来说一下点分十进制的风格是什么,aa.bb.cc.dd.这里的每一个都是一个字节,那么IP地址应该如何设置呢?这里可以使用位段.

struct IP
{
  uint32_t part1 : 8;
  uint32_t part2 : 8;
  uint32_t part3 : 8;
  uint32_t part4 : 8;
};

下面开始说我们的IP地址相关函数的转换,这一系列函数都是为了我们上面的功能,注意有很多,适应不同的场景,我们用的时候注意一下参数和返回值就可以了.

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int inet_aton(const char *cp, struct in_addr *inp);

in_addr_t inet_addr(const char *cp);

in_addr_t inet_network(const char *cp);

char *inet_ntoa(struct in_addr in);

struct in_addr inet_makeaddr(int net, int host);

in_addr_t inet_lnaof(struct in_addr in);

in_addr_t inet_netof(struct in_addr in);

我们要用的函数就是inet_addr,这个函数可以将我们的字符串转换为我们想要的ip地址.那么我想问的是我们ip地址是不是数据?是的,那么网络传输的是不是应该转化为大端?是的,我们需要自己显示的转化为大段吗?不需要,这里这个函数已经帮助我们进行转化了,我们不用在关心

recvfrom

下面继续说我们接收数据的方法,这也是存在一个接口的.

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

下面开始认识一下这些参数,都是非常简单的.

  • sockfd:套接字描述符
  • buf:将数据接收到buf当中
  • len:buf的最大接收能力
  • flags:0阻塞接收

这里还剩下两个参数,请问一件事,我们在接收数据时候是不是应该知道是哪位老兄给我发的消息呢?是的,此时我们需要两个参数来保存我们朋友的信息,这里就是两个输出型参数.

sendto

这是一个发送数据的接口,也是非常的简单.

#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
              const struct sockaddr *dest_addr, socklen_t addrlen);

继续说一下参数,其中有下面几个问题要讨论.

  • sockfd:套接字描述符
  • buf:要发送的数据
  • len:发送数据的长度
  • flags:0 阻塞发送

我们想要发送数据,请问对于客户端而言我们要发送给谁?很简单服务端,也就是我们需要知道服务端的信息,那么我们是如何知道的呢?一个比较厉害的服务端,它的信息是公开的,例如我们知道百度的信息,所有我们知道的.那么如果我们服务端给客户端发信息呢?要知道服务端为何要给客户端发信息,肯定是之前客户端请求服务端了,那么我们服务端已经拿到了客户端的信息,这就是recvform为何要保存朋友信息的作用,形成一个闭环.

基于UDP协议简易聊天室

现在我们已经知道了接口,可是感觉上面我们还是有点空,我们应该做一个简单的东西让大家看一看,这里做一个简单的聊天室,后面我们TCP还要做一个,那么功能比较全.

服务端

关于服务端我们应该如何做?很简单,我们创建套接字,绑定服务端信息,然后等着客户端来信息我们进行处理就可以了.不过由于今天我们的服务器是云服务器,这里有几个问题要和大家谈一下.

#include <iostream>
#include <string>
#include <stdlib.h>
#include <stdio.h>
#include <cstring>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

class UDPServer
{
public:
  UDPServer(uint16_t port, std::string ip = "")
      : _port(port),
        _ip(ip)
  {
  }

  ~UDPServer() {}

public:
  void init()
  {
    // 1.  创建套接字
    _socketFd = socket(AF_INET, SOCK_DGRAM, 0);
    if (_socketFd < 0)
    {
      std::cout << "创建套接字失败 " << strerror(errno) << "socket" << _socketFd << std::endl;
      exit(1);
    }
    std::cout << "创建套接字成功 socket: " << _socketFd << std::endl;

    // 2. 绑定网络信息 包含ip地址 端口号 和 本地或者域间
    struct sockaddr_in local;
    bzero(&local, sizeof local); // 初始化
    local.sin_family = PF_INET;
    local.sin_port = htons(_port);
    local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // INADDR_ANY 就是0

    if (bind(_socketFd, (const sockaddr *)&local, sizeof(local)) == -1)
    {
      std::cout << "UDP 套接字bind 失败  " << strerror(errno) << std::endl;
      exit(2);
    }
    std::cout << "UDP 套接字bind 成功" << std::endl;
  }

  void start()
  {

    while (true)
    {
      std::cout << "服务器已经启动成功" << std::endl;
      sleep(1);
    }
  }

private:
  int _socketFd; // fd
  uint16_t _port;
  std::string _ip;
};

static void Usage(const std::string proc)
{
  std::cout << "Usage:\n\t" << proc << "  port [ip]  " << std::endl;
}

int main(int argc, char *argv[])
{
  if (argc != 2 && argc != 3)
  {
    Usage(argv[0]);
    exit(3);
  }
  int port = atoi(argv[1]);
  std::string ip;
  if (argc == 3)
  {
    ip = argv[2];
  }

  UDPServer ser(port, ip);
  ser.init();
  ser.start();
  return 0;
}

万字解析网络套接字_服务器_04

下面我们回答我们为何没有显示的使用ip地址,注意由于我们服务器是云服务器,我们禁止绑定我们云服务器的ip地址,这是由于我们的ip地址是被转换过的,因此我们让这个服务端自行去绑定吧.

再来说下面的问题,现在我们已经把服务端启动了,这里我先做一个简单的业务,把客户端给我们发送的信息显示出来,这里很简单.

void start()
{

  char inbuffer[1024];  // 读去信息
  char outbuffer[1024]; // 发送信息
  while (true)
  {
    memset(inbuffer, '\0', sizeof(inbuffer));
    // 正常进行网络读写
    // 谁给我发消息,我们怎么会消息
    // 后面两个参数是输出行参数
    struct sockaddr_in peer;      // 输出型参数
    socklen_t len = sizeof(peer); // 输入输出型参数
    ssize_t s = recvfrom(_socketFd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
    if (s > 0)
    {
      inbuffer[s] = 0; // 当做字符串
    }
    else if (s == -1)
    {
      std::cerr << "读取数据失败 " << strerror(errno) << std::endl;
      continue;
    }

    // 读取成功的,除了读取到对方的数据,你还要读取到对方的网络地址[ip:port]
    std::string peerIp = inet_ntoa(peer.sin_addr); // 拿到了对方的IP
    uint16_t peerPort = ntohs(peer.sin_port);      // 拿到了对方的port
    printf("client# [%s:%d]# %s\n", peerIp.c_str(), peerPort, inbuffer);
  }
}

客户端

我们先把客户但给设计出来,这里也是非常简单的,不过我们这里有几个问题要和大家分析.

如果一个客户端要连接服务端,必须知道server对应的ip和port,那么我们如何知道的?我们知道服务端非常优秀的话会被公开.

#include <iostream>
#include <string>
#include <stdlib.h>
#include <iostream>
#include <cstring>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <assert.h>

// ./UDPClient server_ip server_port
static void Usage(std::string name)
{
  std::cout << "Usage:\n\t" << name << " server_ip server_port" << std::endl;
}
int main(int argc, char *argv[])
{
  if (argc != 3)
  {
    Usage(argv[0]);
    exit(1);
  }
  // 1. 根据命令行,设置要访问的服务器IP
  std::string server_ip = argv[1];
  uint16_t server_port = atoi(argv[2]);

  // 2. 创建 客户端的套接字
  int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  assert(sockfd > 0);

  // 3. 填充服务器信息
  struct sockaddr_in server;
  bzero(&server, sizeof server);
  server.sin_family = AF_INET;
  server.sin_port = htons(server_port);
  server.sin_addr.s_addr = inet_addr(server_ip.c_str());

  std::string buffer;
  while (true)
  {
    std::cerr << "Please Enter# ";
    std::getline(std::cin, buffer);
    // 发送消息给server
    //std::cout << buffer <<std::endl;
    sendto(sockfd, buffer.c_str(), buffer.size(), 0,
           (const struct sockaddr *)&server, sizeof(server)); // 首次调用sendto函数的时候,我们的client会自动bind自己的ip和port
  }
  return 0;
}

万字解析网络套接字_网络_05

万字解析网络套接字_#include_06

127.0.0.1是什么?这是可以理解是一个IP地址,这是一个本地环回,可以理解为本主机,由于我们是在一个主机内测试的,所以这里我们就用这个了.

服务端需不需要bind?套接字吗? 不需要.所谓的"不需要",指的是: 不需要用户自己bind端口信息!因为OS会自动给你绑定,你也最好这么做.如果我非要自己bind呢?可以!但是严重不推荐!为什么呢??client很多,不能给客户端bind指定的port,port可能被别的client使用了,你的client就无法启动了.那么server凭什么要bind呢??server提供的服务,必须被所有人知道!server不能随便改变!那么什么时候绑定. 首次调用sendto函数的时候,我们的client会自动bind自己的ip和port.

下面我们这里想要做一个功能?服务端把得到的数据进行大写转化让后写回客户端.

void start()
{

  char inbuffer[1024];  // 读去信息
  char outbuffer[1024]; // 发送信息
  while (true)
  {
    memset(inbuffer, '\0', sizeof(inbuffer));
    memset(outbuffer, '\0', sizeof(inbuffer));
    // 正常进行网络读写
    // 谁给我发消息,我们怎么会消息
    // 后面两个参数是输出行参数
    struct sockaddr_in peer;      // 输出型参数
    socklen_t len = sizeof(peer); // 输入输出型参数

    ssize_t s = recvfrom(_socketFd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
    if (s > 0)
    {
      inbuffer[s] = 0; // 当做字符串
    }
    else if (s == -1)
    {
      std::cerr << "读取数据失败 " << strerror(errno) << std::endl;
      continue;
    }

    // 读取成功的,除了读取到对方的数据,你还要读取到对方的网络地址[ip:port]
    std::string peerIp = inet_ntoa(peer.sin_addr); // 拿到了对方的IP
    uint16_t peerPort = ntohs(peer.sin_port);      // 拿到了对方的port
    printf("client# [%s:%d]# %s\n", peerIp.c_str(), peerPort, inbuffer);
    // fflush(stdout);
    for (int i = 0; i < strlen(inbuffer); i++)
    {
      if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
        outbuffer[i] = toupper(inbuffer[i]);
      else
        outbuffer[i] = toupper(inbuffer[i]);
    }

    sendto(_socketFd, outbuffer, strlen(outbuffer), 0, (const sockaddr *)&peer, sizeof(peer));
  }
}

下面是我们客户端的修改.

int main(int argc, char *argv[])
{
  if (argc != 3)
  {
    Usage(argv[0]);
    exit(1);
  }
  // 1. 根据命令行,设置要访问的服务器IP
  std::string server_ip = argv[1];
  uint16_t server_port = atoi(argv[2]);

  // 2. 创建 客户端的套接字
  int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  assert(sockfd > 0);

  // 3. 填充服务器信息
  struct sockaddr_in server;
  bzero(&server, sizeof server);
  server.sin_family = AF_INET;
  server.sin_port = htons(server_port);
  server.sin_addr.s_addr = inet_addr(server_ip.c_str());

  std::string buffer;
  while (true)
  {
    std::cerr << "Please Enter# ";
    std::getline(std::cin, buffer);
    // 发送消息给server
    // std::cout << buffer <<std::endl;
    sendto(sockfd, buffer.c_str(), buffer.size(), 0,
           (const struct sockaddr *)&server, sizeof(server));

    char inbuffer[1024];
    struct sockaddr_in temp; // 一个辅助的
    socklen_t len = sizeof(temp);
    ssize_t s = recvfrom(sockfd, inbuffer, sizeof(inbuffer), 0, (struct sockaddr *)&temp, &len);
    if (s > 0)
    {
      inbuffer[s] = 0;
      std::cout << "server echo# " << inbuffer << std::endl;
    }
  }
  return 0;
}

万字解析网络套接字_网络_07

这里我们还想要添加一个功能?消息路由,当我们一个发送消息的时候其他人也是可以接收到了,也就是群消息,这里我们称之为消息路由.既然存在消息路由,我们需要保存所有的客户端,这里使用一个哈希表.

class UDPServer
{
public:
  UDPServer(uint16_t port, std::string ip = "")
      : _port(port),
        _ip(ip)
  {
  }

  ~UDPServer() {}

public:
  void init()
  {
      //...
  }

  void start()
  {

    char inbuffer[1024];  // 读去信息
    char outbuffer[1024]; // 发送信息
    while (true)
    {
      memset(inbuffer, '\0', sizeof(inbuffer));
      memset(outbuffer, '\0', sizeof(inbuffer));

      struct sockaddr_in peer;      // 输出型参数
      socklen_t len = sizeof(peer); // 输入输出型参数

      ssize_t s = recvfrom(_socketFd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
      if (s > 0)
      {
        inbuffer[s] = 0; // 当做字符串
      }
      else if (s == -1)
      {
        std::cerr << "读取数据失败 " << strerror(errno) << std::endl;
        continue;
      }

      // 读取成功的,除了读取到对方的数据,你还要读取到对方的网络地址[ip:port]
      std::string peerIp = inet_ntoa(peer.sin_addr); // 拿到了对方的IP
      uint16_t peerPort = ntohs(peer.sin_port);      // 拿到了对方的port
      printf("client# [%s:%d]# %s\n", peerIp.c_str(), peerPort, inbuffer);

      checkOnlineUser(peerIp, peerPort, peer); // 如果存在,什么都不做,如果不存在,就添加
      // fflush(stdout);
      for (int i = 0; i < strlen(inbuffer); i++)
      {
        if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
          outbuffer[i] = toupper(inbuffer[i]);
        else
          outbuffer[i] = toupper(inbuffer[i]);
      }
      messageRoute(peerIp, peerPort, inbuffer); // 消息路由
 
    }
  }

  void checkOnlineUser(std::string &ip, uint32_t port, struct sockaddr_in &peer)
  {
    std::string key = ip;
    key += ":";
    key += std::to_string(port);
    auto iter = users.find(key);
    if (iter == users.end())
    {
      users.insert({key, peer});
    }
    else
    {
      // iter->first, iter->second->
      // do nothing
    }
  }

  void messageRoute(std::string ip, uint32_t port, std::string info)
  {

    std::string message = "[";
    message += ip;
    message += ":";
    message += std::to_string(port);
    message += "]# ";
    message += info;

    for (auto &user : users)
    {
      sendto(_socketFd, message.c_str(), message.size(), 0, (struct sockaddr *)&(user.second), sizeof(user.second));
    }
  }

private:
  int _socketFd; // fd
  uint16_t _port;
  std::string _ip;
  std::unordered_map<std::string, struct sockaddr_in> users; // 保存在线的信心
};

下面是我们的客户端,先试试不用变.

int main(int argc, char *argv[])
{
  if (argc != 3)
  {
    Usage(argv[0]);
    exit(1);
  }
  // 1. 根据命令行,设置要访问的服务器IP
  std::string server_ip = argv[1];
  uint16_t server_port = atoi(argv[2]);

  // 2. 创建 客户端的套接字
  int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  assert(sockfd > 0);

  // 3. 填充服务器信息
  struct sockaddr_in server;
  bzero(&server, sizeof server);
  server.sin_family = AF_INET;
  server.sin_port = htons(server_port);
  server.sin_addr.s_addr = inet_addr(server_ip.c_str());

  std::string buffer;
  while (true)
  {
    std::cerr << "Please Enter# ";
    std::getline(std::cin, buffer);
    // 发送消息给server
    // std::cout << buffer <<std::endl;
    sendto(sockfd, buffer.c_str(), buffer.size(), 0,
           (const struct sockaddr *)&server, sizeof(server));

    char inbuffer[1024];
    struct sockaddr_in temp; // 一个辅助的
    socklen_t len = sizeof(temp);
    ssize_t s = recvfrom(sockfd, inbuffer, sizeof(inbuffer), 0, (struct sockaddr *)&temp, &len);
    if (s > 0)
    {
      inbuffer[s] = 0;
      std::cout << "server echo# " << inbuffer << std::endl;
    }
  }
  return 0;
}

万字解析网络套接字_服务器_08

你会发现,我们我一个客户端在发送消息,可是对于另外一个客户端,我们被阻塞住了,这是因为其他的客户端在等待我们数据数据,所以我们这里客户端使用多线程.

void *recverAndPrint(void *args)
{
    while (true)
    {
        int sockfd = *(int *)args;
        char buffer[1024];
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }
}

int main(int argc, char *argv[])
{
  if (argc != 3)
  {
    Usage(argv[0]);
    exit(1);
  }
  // 1. 根据命令行,设置要访问的服务器IP
  std::string server_ip = argv[1];
  uint16_t server_port = atoi(argv[2]);

  // 2. 创建 客户端的套接字
  int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  assert(sockfd > 0);

  // 3. 填充服务器信息
  struct sockaddr_in server;
  bzero(&server, sizeof server);
  server.sin_family = AF_INET;
  server.sin_port = htons(server_port);
  server.sin_addr.s_addr = inet_addr(server_ip.c_str());

  std::string buffer;
  pthread_t t;
  pthread_create(&t, nullptr, recverAndPrint, (void *)&sockfd);
  while (true)
  {
    std::cerr << "Please Enter# ";
    std::getline(std::cin, buffer);
    // 发送消息给server
    // std::cout << buffer <<std::endl;
    sendto(sockfd, buffer.c_str(), buffer.size(), 0,
           (const struct sockaddr *)&server, sizeof(server));
  }
  return 0;
}

万字解析网络套接字_运维_09

inet_ntoa

本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址,但是我们通常用点分十进制的字符串表示IP 地址,我们这里需要谈一下inet_ntoa ,这个inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?

万字解析网络套接字_#include_10

man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:

万字解析网络套接字_网络_11

运行结果如下:

万字解析网络套接字_#include_12

因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.在APUE中, 明确提出inet_ntoa不是线程安全的函数,但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;

TCP协议

我们来进行下一个传输层协议,就是我们常用的TCP.这个是我们今天的大头.相比较与UDP,TCP更加复杂一点.

socket API

下面我们也应该学习一下我们TCP的一些接口,这里都是非常简单的.

socket

这个和UDP一样,创建一个网络套接字,不过要注意一下我们第二个参数,他是面向字节流的.

万字解析网络套接字_#include

bind

绑定我们的信息,没有什么可以谈的,还是上面的几个结构体.

listen

下面我们说一下TCP不同于UDP的地方,我们在绑定我们的主机信息后,需要监听.我们知道我们现在做的是为了通信,在做任何事情前先干什么?我们需要监听我们套接字使用的状态.

#include <sys/types.h>       
#include <sys/socket.h>

int listen(int sockfd, int backlog);

下面说一下我们我们的参数

  • sockfd:套接字描述符
  • backlog:已完成连接队列的大小,不做解释,后面我们在原理那里分析.

accept

按道理我们已经可以链接了,不过我们是TCP,那么要先获取链接,这个接口就是.首先这个和接口也是一个得到文件描述符的接口,而且这个文件描述符是我们用于通信的

#include <sys/types.h>         
#include <sys/socket.h>

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

先说参数

  • sockfd 监听套接字
  • 后面的两个参数,输出型参数,主要为了保存客户端的信息

我们疑惑了,我们在socket哪里不是得到了一个套接字吗?是的,但是我们这里不用他进行通信,就像是在一个饭店里,我们存在前台服务员,他的作用就是拉客,当客人拉到后,会有专门的服务员给客人提供服务.其中前台服务员就是socket得到的套接字,我们这里称之为监听套接字,专门服务的就是accept的套接字,他的作用就是通信.那么请问获取连接失败后会怎么样呢?不会怎么样,失败就失败了吧.

connect

这个接口主要就是给客户端使用的,和服务端进行通信.

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

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

继续说我们的参数

  • sockfd:套接字描述符
  • addr:服务端地址信息结构(服务端IP,服务端的端口)
  • addrlen:服务端地址信息结构的长度

read & write

由于我们的TCP是面向字节流的,那么我们是可以按照文件操作进行信息的传递,这个和我们之前在基础IO那里的用法一样.

send & recv

TCP为了通信提供了两套接口,我们选择任意一套就可以了,我推荐上面的那套,不过这一套我们也看一下就可以了.

#include <sys/types.h>
#include <sys/socket.h>
// 发送数据 flags 直接置为0,阻塞
ssize_t send(int sockfd, const void *buf, size_t len, int flags);


// 接收数据 flags 直接置为0,阻塞
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

基于TCP协议大小写转换

下面我们我们也用TCP协议写一个简单的聊天室,只不过这一次我们把这个消息路由的功能给抽离出来,后面我们还需要这个TCP的框架做其他的功能.

下面的代码是一个日志打印函数.

// log.hpp
#pragma once

#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <stdlib.h>

#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3

const char *log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};

// logMessage(DEBUG, "%d", 10);
void logMessage(int level, const char *format, ...)
{
  assert(level >= DEBUG);
  assert(level <= FATAL);

  char *name = getenv("USER");

  char logInfo[1024];
  va_list ap; // ap -> char*
  va_start(ap, format);

  vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);

  va_end(ap); // ap = NULL

  FILE *out = (level == FATAL) ? stderr : stdout;

  fprintf(out, "%s | %u | %s | %s\n",
          log_level[level],
          (unsigned int)time(nullptr),
          name == nullptr ? "unknow" : name,
          logInfo);
}

这个头文件是我们头文件的集合

//util.hpp
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <cassert>
#include <ctype.h>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"

#define SOCKET_ERR 1
#define BIND_ERR 2
#define LISTEN_ERR 3
#define USAGE_ERR 4
#define CONN_ERR 5

#define BUFFER_SIZE 1024

服务端

服务端很简单,我们注意一下监听就可以可以了.

#include "util.hpp"
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>

class ServerTCP
{
public:
  ServerTCP(uint16_t port, const std::string &ip = "") : port_(port), ip_(ip), listenSock_(-1)
  {
  }
  ~ServerTCP()
  {
  }

public:
  void init()
  {
    // 1. 创建socket
    listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
    if (listenSock_ < 0)
    {
      logMessage(FATAL, "socket: %s", strerror(errno));
      exit(SOCKET_ERR);
    }
    logMessage(DEBUG, "socket: %s, %d", strerror(errno), listenSock_);

    // 2. bind绑定
    // 2.1 填充服务器信息
    struct sockaddr_in local; // 用户栈
    memset(&local, 0, sizeof local);
    local.sin_family = PF_INET;
    local.sin_port = htons(port_);
    ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
    // 2.2 本地socket信息,写入sock_对应的内核区域
    if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
    {
      logMessage(FATAL, "bind: %s", strerror(errno));
      exit(BIND_ERR);
    }
    logMessage(DEBUG, "bind: %s, %d", strerror(errno), listenSock_);

    // 3. 监听socket,为何要监听呢?TCP是面向连接的!
    if (listen(listenSock_, 5 /*后面再说*/) < 0)
    {
      logMessage(FATAL, "listen: %s", strerror(errno));
      exit(LISTEN_ERR);
    }
    logMessage(DEBUG, "listen: %s, %d", strerror(errno), listenSock_);
    // 运行别人来连接你了
  }

  void loop()
  {

    while (true)
    {
      struct sockaddr_in peer;
      socklen_t len = sizeof(peer);
      // 4. 获取连接, accept 的返回值是一个新的socket fd ??
      int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
      if (serviceSock < 0)
      {
        // 获取链接失败
        logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);
        continue;
      }
      // 4.1 获取客户端基本信息
      uint16_t peerPort = ntohs(peer.sin_port);
      std::string peerIp = inet_ntoa(peer.sin_addr);

      logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d",
                 strerror(errno), peerIp.c_str(), peerPort, serviceSock);
      // 大小写转换
      transService(serviceSock, peerIp, peerPort);
    }
  }
  // 大小写转化服务
  // TCP && UDP: 支持全双工
  void transService(int sock, const std::string &clientIp, uint16_t clientPort)
  {
    assert(sock >= 0);
    assert(!clientIp.empty());
    assert(clientPort >= 1024);

    char inbuffer[BUFFER_SIZE];
    while (true)
    {
      ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); // 我们认为我们读到的都是字符串
      if (s > 0)
      {
        // read success
        inbuffer[s] = '\0';
        if (strcasecmp(inbuffer, "quit") == 0)
        {
          logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
          break;
        }
        logMessage(DEBUG, "trans before: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);
        // 可以进行大小写转化了
        for (int i = 0; i < s; i++)
        {
          if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
            inbuffer[i] = toupper(inbuffer[i]);
        }
        logMessage(DEBUG, "trans after: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);
        // 写回去
        write(sock, inbuffer, strlen(inbuffer));
      }
      else if (s == 0)
      {
        // pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭
        // s == 0: 代表对方关闭,client 退出
        logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
        break;
      }
      else
      {
        logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno));
        break;
      }
    }

    // 只要走到这里,一定是client退出了,服务到此结束
    close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏!
    logMessage(DEBUG, "server close %d done", sock);
  }

private:
  // sock
  int listenSock_;
  // port
  uint16_t port_;
  // ip
  std::string ip_;
};

static void Usage(std::string proc)
{
  std::cerr << "Usage:\n\t" << proc << " port ip" << std::endl;
  std::cerr << "example:\n\t" << proc << " 8080 127.0.0.1\n"
            << std::endl;
}

// ./ServerTCP local_port local_ip
int main(int argc, char *argv[])
{
  if (argc != 2 && argc != 3)
  {
    Usage(argv[0]);
    exit(USAGE_ERR);
  }
  uint16_t port = atoi(argv[1]);
  std::string ip;
  if (argc == 3)
    ip = argv[2];

  ServerTCP svr(port, ip);
  svr.init();
  svr.loop();
  return 0;
}

客户端

和UDP一眼,我们客户端不需要bind,不需要监听,不需要accept.

#include "util.hpp"
// 2. 需要bind吗??需要,但是不需要自己显示的bind! 不要自己bind!!!!
// 3. 需要listen吗?不需要的!
// 4. 需要accept吗?不需要的!

volatile bool quit = false;

static void Usage(std::string proc)
{
  std::cerr << "Usage:\n\t" << proc << " serverIp serverPort" << std::endl;
  std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8081\n"
            << std::endl;
}
// ./clientTCP serverIp serverPort
int main(int argc, char *argv[])
{
  if (argc != 3)
  {
    Usage(argv[0]);
    exit(USAGE_ERR);
  }
  std::string serverIp = argv[1];
  uint16_t serverPort = atoi(argv[2]);

  // 1. 创建socket SOCK_STREAM
  int sock = socket(AF_INET, SOCK_STREAM, 0);
  if (sock < 0)
  {
    std::cerr << "socket: " << strerror(errno) << std::endl;
    exit(SOCKET_ERR);
  }

  // 2. connect,发起链接请求,你想谁发起请求呢??当然是向服务器发起请求喽
  // 2.1 先填充需要连接的远端主机的基本信息
  struct sockaddr_in server;
  memset(&server, 0, sizeof(server));
  server.sin_family = AF_INET;
  server.sin_port = htons(serverPort);
  inet_aton(serverIp.c_str(), &server.sin_addr);

  // 2.2 发起请求,connect 会自动帮我们进行bind!
  if (connect(sock, (const struct sockaddr *)&server, sizeof(server)) != 0)
  {
    std::cerr << "connect: " << strerror(errno) << std::endl;
    exit(CONN_ERR);
  }
  
  std::cout << "info : connect success: " << sock << std::endl;

  std::string message;
  while (!quit)
  {
    message.clear();
    std::cout << "请输入你的消息>>> ";
    std::getline(std::cin, message);
    if (strcasecmp(message.c_str(), "quit") == 0)
      quit = true;

    ssize_t s = write(sock, message.c_str(), message.size());
    if (s > 0)
    {
      message.resize(1024);
      ssize_t s = read(sock, (char *)(message.c_str()), 1024);
      if (s > 0)
        message[s] = 0;
      std::cout << "Server Echo>>> " << message << std::endl;
    }
    else if (s <= 0)
    {
      break;
    }
  }
  close(sock);
  return 0;
}

万字解析网络套接字_网络_14

不过我们要谈一个问题,假设我们的是第一个客户端,这里服务端直接accept拿到套接字,我们进行通信,此时由于我们write和read都是阻塞式,那么这里的transService服务就被阻塞住了,我们服务端额主执行流被阻塞了,不能继续where循环拿到另外的客户端了.

万字解析网络套接字_运维_15

多进程版本

那么此时我们需要更新一个版本,这里先用多进程,这里有很多的的问题谈论,关于多进程我们这里提供两个版本

void loop()
{

    while (true)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        // 4. 获取连接, accept 的返回值是一个新的socket fd ??
        int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
        if (serviceSock < 0)
        {
            // 获取链接失败
            logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);
            continue;
        }
        // 4.1 获取客户端基本信息
        uint16_t peerPort = ntohs(peer.sin_port);
        std::string peerIp = inet_ntoa(peer.sin_addr);

        logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d",
                   strerror(errno), peerIp.c_str(), peerPort, serviceSock);

        // 5.1 v1 版本-- 多进程版本-- 父进程打开的文件会被子进程继承吗?会的
        pid_t id = fork();
        assert(id != -1);
        if (id == 0)
        {
            close(listenSock_); // 建议
            // 子进程
            transService(serviceSock, peerIp, peerPort);
            exit(0); // 进入僵尸
        }
        // 父进程
        close(serviceSock); // 这一步是一定要做的!
    }
}

先说父进程为何一定要关闭serviceSock文件描述符,因为父进程只需要获得文件描述符,子进程才是才是经行通信的,一个OS的文件描述符是有限的,故我们最好关闭,否则一但客户端多了我们这个服务端就出现了问题,文件描述符不够用

万字解析网络套接字_IP_16

上面我们会出现问题,我们出现了僵尸进程.那么我们会说我们在主进程阻塞等待不就可以了吗?请问如果我们主进程阻塞,我们主执行流六还要往下执行吗? 不能的,我们被阻塞了.

  • 使用非阻塞,不过需要一个表保存我们所有的子进程,轮询检测
  • 忽略SIGCHLD信号,原理在信号哪里已经分析,仅在Linux下有效

万字解析网络套接字_#include_17

忽略SIGCHLD

子进程退出会给父进程发送一个SIGCHLD信息,我们这个已经谈过了,注意这个仅在Linux下有效.

void loop()
{
    signal(SIGCHLD, SIG_IGN); // only Linux

    while (true)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        // 4. 获取连接, accept 的返回值是一个新的socket fd ??
        int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
        if (serviceSock < 0)
        {
            // 获取链接失败
            logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);
            continue;
        }
        // 4.1 获取客户端基本信息
        uint16_t peerPort = ntohs(peer.sin_port);
        std::string peerIp = inet_ntoa(peer.sin_addr);

        logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d",
                   strerror(errno), peerIp.c_str(), peerPort, serviceSock);

        // 5.1 v1 版本-- 多进程版本-- 父进程打开的文件会被子进程继承吗?会的
        pid_t id = fork();
        assert(id != -1);
        if (id == 0)
        {
            close(listenSock_); // 建议
            // 子进程
            transService(serviceSock, peerIp, peerPort);
            exit(0); // 进入僵尸
        }
        // 父进程
        close(serviceSock); // 这一步是一定要做的!
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DznCPkgB-1679408268656)(D:/%E5%8D%9A%E5%AE%A2/%E5%8D%9A%E5%AE%A2%E5%9B%BE%E7%89%87/image-20230320114855917.png)]

孤儿进程

我们知道孤儿进程会被bash进程进行管理,资源也会被回收,所有这里我们也用这个方法.

void loop()
{

    while (true)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        // 4. 获取连接, accept 的返回值是一个新的socket fd ??
        int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
        if (serviceSock < 0)
        {
            // 获取链接失败
            logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);
            continue;
        }
        // 4.1 获取客户端基本信息
        uint16_t peerPort = ntohs(peer.sin_port);
        std::string peerIp = inet_ntoa(peer.sin_addr);

        logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d",
                   strerror(errno), peerIp.c_str(), peerPort, serviceSock);

        // 5.1 v1 .1 版本-- 多进程版本-- 也是可以的
        // 爷爷进程
        pid_t id = fork();
        if (id == 0)
        {
            // 爸爸进程
            close(listenSock_); // 建议
            // 又进行了一次fork,让 爸爸进程
            if (fork() > 0)
                exit(0);
            // 孙子进程 -- 就没有爸爸 -- 孤儿进程 -- 被系统领养 -- 回收问题就交给了系统来回收
            transService(serviceSock, peerIp, peerPort);
            exit(0);
        }
        // 父进程
        close(serviceSock); // 这一步是一定要做的!
        // 爸爸进程直接终止,立马得到退出码,释放僵尸进程状态
        pid_t ret = waitpid(id, nullptr, 0); // 就用阻塞式
        assert(ret > 0);
        (void)ret;
    }
}

我们父进程的作用就是创建孙子进程,所以它一进去就可以退出,爷爷进程在这里等待父进程的资源,回收就可以了

万字解析网络套接字_IP_18

多线程版本

下面我们开始多线程版本,需要做一点辅助,使用几个结构体,引入引入线程分离

class ServerTCP; // 申明一下ServerTCP

class ThreadData
{
public:
  uint16_t clientPort_;
  std::string clinetIp_;
  int sock_;
  ServerTCP *this_;

public:
  ThreadData(uint16_t port, std::string ip, int sock, ServerTCP *ts)
      : clientPort_(port), clinetIp_(ip), sock_(sock), this_(ts)
  {
  }
};

class ServerTCP
{
public:
  ServerTCP(uint16_t port, const std::string &ip = "") : port_(port), ip_(ip), listenSock_(-1)
  {
  }
  ~ServerTCP()
  {
  }

public:
  void init()
  {
    // 1. 创建socket
    listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
    if (listenSock_ < 0)
    {
      logMessage(FATAL, "socket: %s", strerror(errno));
      exit(SOCKET_ERR);
    }
    logMessage(DEBUG, "socket: %s, %d", strerror(errno), listenSock_);

    // 2. bind绑定
    // 2.1 填充服务器信息
    struct sockaddr_in local; // 用户栈
    memset(&local, 0, sizeof local);
    local.sin_family = PF_INET;
    local.sin_port = htons(port_);
    ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
    // 2.2 本地socket信息,写入sock_对应的内核区域
    if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
    {
      logMessage(FATAL, "bind: %s", strerror(errno));
      exit(BIND_ERR);
    }
    logMessage(DEBUG, "bind: %s, %d", strerror(errno), listenSock_);

    // 3. 监听socket,为何要监听呢?TCP是面向连接的!
    if (listen(listenSock_, 5 /*后面再说*/) < 0)
    {
      logMessage(FATAL, "listen: %s", strerror(errno));
      exit(LISTEN_ERR);
    }
    logMessage(DEBUG, "listen: %s, %d", strerror(errno), listenSock_);
    // 运行别人来连接你了
  }
  static void *threadRoutine(void *args)
  {
    pthread_detach(pthread_self()); // 设置线程分离
    ThreadData *td = static_cast<ThreadData *>(args);
    td->this_->transService(td->sock_, td->clinetIp_, td->clientPort_);
    delete td;
    return nullptr;
  }
  void loop()
  {
    while (true)
    {
      struct sockaddr_in peer;
      socklen_t len = sizeof(peer);
      // 4. 获取连接, accept 的返回值是一个新的socket fd ??
      int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
      if (serviceSock < 0)
      {
        // 获取链接失败
        logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);
        continue;
      }
      // 4.1 获取客户端基本信息
      uint16_t peerPort = ntohs(peer.sin_port);
      std::string peerIp = inet_ntoa(peer.sin_addr);

      logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d",
                 strerror(errno), peerIp.c_str(), peerPort, serviceSock);
       
      // 多线程版本
      ThreadData *td = new ThreadData(peerPort, peerIp, serviceSock, this);
      pthread_t tid;
      pthread_create(&tid, nullptr, threadRoutine, (void *)td);
    }
  }
  // 大小写转化服务
  // TCP && UDP: 支持全双工
  void transService(int sock, const std::string &clientIp, uint16_t clientPort)
  {
    assert(sock >= 0);
    assert(!clientIp.empty());
    assert(clientPort >= 1024);

    char inbuffer[BUFFER_SIZE];
    while (true)
    {
      ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); // 我们认为我们读到的都是字符串
      if (s > 0)
      {
        // read success
        inbuffer[s] = '\0';
        if (strcasecmp(inbuffer, "quit") == 0)
        {
          logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
          break;
        }
        logMessage(DEBUG, "trans before: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);
        // 可以进行大小写转化了
        for (int i = 0; i < s; i++)
        {
          if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
            inbuffer[i] = toupper(inbuffer[i]);
        }
        logMessage(DEBUG, "trans after: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);
        // 写回去
        write(sock, inbuffer, strlen(inbuffer));
      }
      else if (s == 0)
      {
        // pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭
        // s == 0: 代表对方关闭,client 退出
        logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
        break;
      }
      else
      {
        logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno));
        break;
      }
    }

    // 只要走到这里,一定是client退出了,服务到此结束
    close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏!
    logMessage(DEBUG, "server close %d done", sock);
  }

private:
  // sock
  int listenSock_;
  // port
  uint16_t port_;
  // ip
  std::string ip_;
};

万字解析网络套接字_运维_19

线程池版本

我们需要引入我们之前的线程池,把处理任务的函数抽离出来,这里需要封装一个任务结构体.

// 任务

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include "log.hpp"

class Task
{
public:
    //等价于
    // typedef std::function<void (int, std::string, uint16_t)> callback_t;
    using callback_t = std::function<void (int, std::string, uint16_t)>;
private:
    int sock_; // 给用户提供IO服务的sock
    uint16_t port_;  // client port
    std::string ip_; // client ip
    callback_t func_;  // 回调方法
public:
    Task():sock_(-1), port_(-1)
    {}
    Task(int sock, std::string ip, uint16_t port, callback_t func)
    : sock_(sock), ip_(ip), port_(port), func_(func)
    {}
    void operator () ()
    {
        logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 开始啦...",\
            pthread_self(), ip_.c_str(), port_);

        func_(sock_, ip_, port_);

        logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 结束啦...",\
            pthread_self(), ip_.c_str(), port_);
    }
    ~Task()
    {}
};

// server
void transService(int sock, const std::string &clientIp, uint16_t clientPort)
{
  assert(sock >= 0);
  assert(!clientIp.empty());
  assert(clientPort >= 1024);

  char inbuffer[BUFFER_SIZE];
  while (true)
  {
    ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); // 我们认为我们读到的都是字符串
    if (s > 0)
    {
      // read success
      inbuffer[s] = '\0';
      if (strcasecmp(inbuffer, "quit") == 0)
      {
        logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
        break;
      }
      logMessage(DEBUG, "trans before: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);
      // 可以进行大小写转化了
      for (int i = 0; i < s; i++)
      {
        if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
          inbuffer[i] = toupper(inbuffer[i]);
      }
      logMessage(DEBUG, "trans after: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);

      write(sock, inbuffer, strlen(inbuffer));
    }
    else if (s == 0)
    {
      // pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭
      // s == 0: 代表对方关闭,client 退出
      logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
      break;
    }
    else
    {
      logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno));
      break;
    }
  }

  // 只要走到这里,一定是client退出了,服务到此结束
  close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏!
  logMessage(DEBUG, "server close %d done", sock);
}

class ServerTCP
{
public:
  ServerTCP(uint16_t port, const std::string &ip = "")
      : port_(port),
        ip_(ip),
        listenSock_(-1),
        tp_(nullptr)
  {
  }
  ~ServerTCP()
  {
  }

public:
  void init()
  {
    // 1. 创建socket
    listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
    if (listenSock_ < 0)
    {
      logMessage(FATAL, "socket: %s", strerror(errno));
      exit(SOCKET_ERR);
    }
    logMessage(DEBUG, "socket: %s, %d", strerror(errno), listenSock_);

    // 2. bind绑定
    // 2.1 填充服务器信息
    struct sockaddr_in local; // 用户栈
    memset(&local, 0, sizeof local);
    local.sin_family = PF_INET;
    local.sin_port = htons(port_);
    ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
    // 2.2 本地socket信息,写入sock_对应的内核区域
    if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
    {
      logMessage(FATAL, "bind: %s", strerror(errno));
      exit(BIND_ERR);
    }
    logMessage(DEBUG, "bind: %s, %d", strerror(errno), listenSock_);

    // 3. 监听socket,为何要监听呢?TCP是面向连接的!
    if (listen(listenSock_, 5 /*后面再说*/) < 0)
    {
      logMessage(FATAL, "listen: %s", strerror(errno));
      exit(LISTEN_ERR);
    }
    logMessage(DEBUG, "listen: %s, %d", strerror(errno), listenSock_);
    // 运行别人来连接你了

    // 4. 加载线程池
    tp_ = ThreadPool<Task>::getInstance();
  }

  void loop()
  {
    tp_->start();
    logMessage(DEBUG, "thread pool start success, thread num: %d", tp_->threadNum());
    while (true)
    {
      struct sockaddr_in peer;
      socklen_t len = sizeof(peer);
      // 4. 获取连接, accept 的返回值是一个新的socket fd ??
      // 4.1 listenSock_: 监听 && 获取新的链接-> sock
      // 4.2 serviceSock: 给用户提供新的socket服务
      int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
      if (serviceSock < 0)
      {
        // 获取链接失败
        logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);
        continue;
      }
      // 4.1 获取客户端基本信息
      uint16_t peerPort = ntohs(peer.sin_port);
      std::string peerIp = inet_ntoa(peer.sin_addr);

      logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d",
                 strerror(errno), peerIp.c_str(), peerPort, serviceSock);
      
      // 创建任务
      Task t(serviceSock, peerIp, peerPort, transService);
      // 提交任务
      tp_->push(t);
    }
  }

private:
  // sock
  int listenSock_;
  // port
  uint16_t port_;
  // ip
  std::string ip_;
  // 引入线程池
  ThreadPool<Task> *tp_;
};

万字解析网络套接字_运维_20

popen

下面来个我们可以通过远成操作我们服务器的任务,这里说一个函数.

NAME
       popen, pclose - pipe stream to or from a process

SYNOPSIS
       #include <stdio.h>

       FILE *popen(const char *command, const char *type);

总而言之,popen()可以执行shell命令,并读取此命令的返回值

void execCommand(int sock, const std::string &clientIp, uint16_t clientPort)
{
    assert(sock >= 0);
    assert(!clientIp.empty());
    assert(clientPort >= 1024);

    char command[BUFFER_SIZE];
    while (true)
    {
        ssize_t s = read(sock, command, sizeof(command) - 1); //我们认为我们读到的都是字符串
        if (s > 0)
        {
            command[s] = '\0';
            logMessage(DEBUG, "[%s:%d] exec [%s]", clientIp.c_str(), clientPort, command);
            // 考虑安全
            std::string safe = command;
            if((std::string::npos != safe.find("rm")) || (std::string::npos != safe.find("unlink")))
            {
                break;
            }
            // 我们是以r方式打开的文件,没有写入
            // 所以我们无法通过dup的方式得到对应的结果
            FILE *fp = popen(command, "r");  
            if(fp == nullptr)
            {
                logMessage(WARINING, "exec %s failed, beacuse: %s", command, strerror(errno));
                break;
            }
            char line[1024];
            while(fgets(line, sizeof(line)-1, fp) != nullptr)
            {
                write(sock, line, strlen(line));
            }
            // dup2(fd, 1);
            // dup2(sock, fp->_fileno);
            // fflush(fp);
            pclose(fp);
            logMessage(DEBUG, "[%s:%d] exec [%s] ... done", clientIp.c_str(), clientPort, command);
        }
        else if (s == 0)
        {
            // pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭
            // s == 0: 代表对方关闭,client 退出
            logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
            break;
        }
        else
        {
            logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno));
            break;
        }
    }

    // 只要走到这里,一定是client退出了,服务到此结束
    close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏!
    logMessage(DEBUG, "server close %d done", sock);
}

class ServerTCP
{
public:
    ServerTCP(uint16_t port, const std::string &ip = "")
        : port_(port),
          ip_(ip),
          listenSock_(-1),
          tp_(nullptr)
    {
    }
    ~ServerTCP()
    {
    }

public:
    void init()
    {
       //...
    }

    void loop()
    {
        // signal(SIGCHLD, SIG_IGN); // only Linux
        tp_->start();
        logMessage(DEBUG, "thread pool start success, thread num: %d", tp_->threadNum());
        while (true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 4. 获取连接, accept 的返回值是一个新的socket fd ??
            // 4.1 listenSock_: 监听 && 获取新的链接-> sock
            // 4.2 serviceSock: 给用户提供新的socket服务
            int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
            if (serviceSock < 0)
            {
                // 获取链接失败
                logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);
                continue;
            }
            // 4.1 获取客户端基本信息
            uint16_t peerPort = ntohs(peer.sin_port);
            std::string peerIp = inet_ntoa(peer.sin_addr);

            logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d",
                       strerror(errno), peerIp.c_str(), peerPort, serviceSock);
   
            Task t(serviceSock, peerIp, peerPort, execCommand);
            tp_->push(t);

  
        }
    }

private:
    // sock
    int listenSock_;
    // port
    uint16_t port_;
    // ip
    std::string ip_;
    // 引入线程池
    ThreadPool<Task> *tp_;
};

static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " port ip" << std::endl;
    std::cerr << "example:\n\t" << proc << " 8080 127.0.0.1\n"
              << std::endl;
}

万字解析网络套接字_运维_21

这里我们要滞留一个问题,再有客户端的情况下,你把服务器关闭后立马重启试一下,会出现下面的情况,我先说解决方法,换一个端口,或者等一段时间继续用这个端口,至于为什么出现这个情况我们先不谈,放在下一个博客.

万字解析网络套接字_运维_22

TCP通讯流程

TCP是面向连接的,我们虽然不懂,但是我们是知道的.那么TCP是如何建立链接的呢?很简单,TCP建立连接的是三次握手,结束连接是四次挥手

三次握手&四次挥手

首先我们先来感性的认识一下,后面的原理我们后面谈.

万字解析网络套接字_运维_23

那么请问我们为何是三次握手,又为什么是四次挥手?这个问题我们后面谈.

精灵进程

我们为何可以登上云服务器?我们用指令查看我们的进程,这是由于以d结尾的是我们云服务器进程

万字解析网络套接字_#include_24

先说一下这些条目都是什么.

  • UID 是哪个进程启动的
  • TPGID  如果是-1就代表和终端没有关系,具体数字就和中断有关
  • TTY 是哪一个中断
  • PGID  当前进程所属的进程组
  • SID 会话

下面我们就要谈一下什么是守护进程.

守护进程也叫精灵进程,是一种特殊的进程,一般在后台运行,不与任何控制终端相关联,并且周期性地执行某种任务或等待处理某些发生的事件(处理一些系统级的任务).守护进程通常在系统启动时就运行,它们以 root 用户或者其他特殊的用户运行(例如 apache).

进程组

每个进程都属于某个进程组,进程组是由一个或多个相互间有关联的进程组成的,它的目的是为了进行作业控制.一般而言,启动多个进程,第一个进程是进程组的组长.

万字解析网络套接字_运维_25

会话

首先我们要说的是一个会话可以包含多个进程组,但是一个会话只能存在一个前台进程组,这就是我们为何把sleep进程调到后台我们仍旧可以使用指令的原因,一般而言我们fork创建的子进程都属于当前的会话.

万字解析网络套接字_网络_26

nohup

这里有两个方法,一个是指令,另外一个是函数,我非常推荐函数.

NOHUP(1)                                      User Commands                                      NOHUP(1)

NAME
       nohup - run a command immune to hangups, with output to a non-tty

SYNOPSIS
       nohup COMMAND [ARG]...
       nohup OPTION

那么我们应该如何用呢?很简单,先来个代码.

#include<iostream>
#include<cstdio>
#include<unistd.h>
int main()
{
    while(true)
    {
        std::cout<<"hello rose"<<std::endl;
        sleep(1);

    }
    return 0;
}

万字解析网络套接字_运维_27

setsid

我们应该把自己的网络服务脱离我们的当前的绘画,我们因该有一个独立的绘画,下面存在一个函数

NAME
       setsid - creates a session and sets the process group ID

SYNOPSIS
       #include <unistd.h>

       pid_t setsid(void);

DESCRIPTION
       setsid()  creates a new session if the calling process is not a process group leader.  The calling
       process is the leader of the new session, the process group leader of the new process  group,  and
       has  no  controlling terminal.  The process group ID and session ID of the calling process are set
       to the PID of the calling process.  The calling process will be  the  only  process  in  this  new
       process group and in this new session.

注意,这个函数不应该是我们的进程组组长调用,所以我们这里创建子进程.

#pragma once

#include <cstdio>
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

void daemonize()
{
    int fd = 0;
    // 1. 忽略SIGPIPE
    signal(SIGPIPE, SIG_IGN);
    // 2. 更改进程的工作目录
    // chdir();
    // 3. 让自己不要成为进程组组长
    if (fork() > 0)
        exit(0);
    // 4. 设置自己是一个独立的会话
    setsid();
    // 5. 重定向0,1,2
    if ((fd = open("/dev/null", O_RDWR)) != -1) // fd == 3
    {
        dup2(fd, STDIN_FILENO);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);
        // 6. 关闭掉不需要的fd
        if(fd > STDERR_FILENO) close(fd);
    }
    // 6. close(0,1,2)// 严重不推荐
}