Linux网络编程从入门到实战:UDP/TCP协议与socket编程全解析

Linux网络编程从入门到实战:UDP/TCP协议与socket编程全解析

    • 一、网络基础
      • [1. 网络发展背景](#1. 网络发展背景)
      • [2. 认识协议](#2. 认识协议)
      • [3. 协议分层](#3. 协议分层)
      • [4. OSI七层模型](#4. OSI七层模型)
      • [5. TCP/IP五层(或四层)模型](#5. TCP/IP五层(或四层)模型)
      • [6. 网络传输基本流程](#6. 网络传输基本流程)
      • [7. 网络中的地址管理](#7. 网络中的地址管理)
    • 二、网络编程基础
      • [1. 理解源IP地址和目的IP地址](#1. 理解源IP地址和目的IP地址)
      • [2. 认识端口号](#2. 认识端口号)
      • [3. 认识TCP协议](#3. 认识TCP协议)
      • [4. 认识UDP协议](#4. 认识UDP协议)
      • [5. 网络字节序](#5. 网络字节序)
      • [6. socket编程接口](#6. socket编程接口)
    • 三、UDP网络程序
      • [1. 封装UdpSocket](#1. 封装UdpSocket)
      • [2. 实现英译汉服务器](#2. 实现英译汉服务器)
      • [3. UDP通用客户端](#3. UDP通用客户端)
      • [4. 地址转换函数](#4. 地址转换函数)
      • [5. UDP群聊](#5. UDP群聊)
    • 四、TCP网络程序
      • [1. TCP socket API详解](#1. TCP socket API详解)
      • [2. 封装TCP socket](#2. 封装TCP socket)
      • [3. TCP通用服务器](#3. TCP通用服务器)
      • [4. TCP多进程版本](#4. TCP多进程版本)
      • [5. TCP多线程版本](#5. TCP多线程版本)
      • [6. 线程池版本TCP服务器](#6. 线程池版本TCP服务器)
      • [7. 守护进程](#7. 守护进程)
    • 五、TCP通信流程和协议定制
      • [1. TCP通信流程](#1. TCP通信流程)
      • [2. 协议定制](#2. 协议定制)

一、网络基础

1. 网络发展背景

独立模式:计算机之间相互独立,每个终端各自持有客户数据。

网络互联:多台计算机连接在一起,完成数据共享。

局域网LAN:计算机数量更多了,通过交换机和路由器连接在一起。

广域网WAN:将远隔千里的计算机都连在一起。

所谓"局域网"和"广域网"只是一个相对的概念。比如我们有"天朝特色"的广域网,也可以看做一个比较大的局域网。

2. 认识协议

计算机之间的传输媒介是光信号和电信号。通过"频率"和"强弱"来表示0和1这样的信息。要想传递各种不同的信息,就需要约定好双方的数据格式。

计算机生产厂商有很多;计算机操作系统也有很多;计算机网络硬件设备还是有很多;如何让这些不同厂商之间生产的计算机能够相互顺畅的通信?就需要有人站出来,约定一个共同的标准,大家都来遵守,这就是网络协议。

3. 协议分层

协议在将来设计的时候是分层的,因为分层后可以实现不同层的解耦,如果有一层出问题不会影响其他层,这是低耦合。

一般我们的分层依据:功能要集中耦合度比较高的模块,这个是高内聚。每一层都要解决特定的问题。

分层的目的是解决不同的问题:

  1. 如何把数据交付到和自己相连的下一台主机上
  2. 如何选择路径的能力
  3. 如何在出错后有纠错的能力
  4. 解决应用方面的问题

为了解决这四个问题,为了解耦就分为了不同层,所以不同层有不同层的协议,每一层的协议解决该层自己的问题。

分层最大的好处在于"封装"。

4. OSI七层模型

  • OSI(Open System Interconnection,开放系统互连)七层网络模型称为开放式系统互联参考模型,是一个逻辑上的定义和规范。
  • 把网络从逻辑上分为了7层。每一层都有相关、相对应的物理设备,比如路由器、交换机。
  • OSI七层模型是一种框架性的设计方法,其最主要的功能就是帮助不同类型的主机实现数据传输。
  • 它的最大优点是将服务、接口和协议这三个概念明确地区分开来,概念清楚,理论也比较完整。通过七个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯。
  • 但是,它既复杂又不实用;所以我们按照TCP/IP四层模型来讲解。

5. TCP/IP五层(或四层)模型

TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇。

TCP/IP通讯协议采用了5层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求。

  • 物理层:负责光/电信号的传递方式。比如现在以太网通用的网线(双绞线)、早期以太网采用的同轴电缆、光纤,现在的wifi无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等。集线器(Hub)工作在物理层。
  • 数据链路层:负责设备之间的数据帧的传送和识别。例如网卡设备的驱动、帧同步、冲突检测、数据差错校验等工作。有以太网、令牌环网、无线LAN等标准。交换机(Switch)工作在数据链路层。
  • 网络层:负责地址管理和路由选择。例如在IP协议中,通过IP地址来标识一台主机,并通过路由表的方式规划出两台主机之间的数据传输的线路(路由)。路由器(Router)工作在网路层。
  • 传输层:负责两台主机之间的数据传输。如传输控制协议(TCP),能够确保数据可靠的从源主机发送到目标主机。
  • 应用层:负责应用程序间沟通,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。我们的网络编程主要就是针对应用层。

物理层我们考虑的比较少。因此很多时候也可以称为TCP/IP四层模型。

一般而言:

  • 对于一台主机,它的操作系统内核实现了从传输层到物理层的内容。
  • 对于一台路由器,它实现了从网络层到物理层。
  • 对于一台交换机,它实现了从数据链路层到物理层。
  • 对于集线器,它只实现了物理层。

但是并不绝对。很多交换机也实现了网络层的转发;很多路由器也实现了部分传输层的内容(比如端口转发)。

6. 网络传输基本流程

同一个网段内的两台主机进行文件传输:

应用层:Telnet、FTP和e-mail等

传输层:TCP和UDP

网络层:IP、ICMP和IGMP

链路层:设备驱动程序及接口卡

两台计算机通过TCP/IP协议通讯的过程如下所示:

跨网段的主机的文件传输:数据从一台计算机到另一台计算机传输过程中要经过一个或多个路由器。

数据包封装和分用:

  • 不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报(datagram),在链路层叫做帧(frame)。
  • 应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装(Encapsulation)。
  • 首部信息中包含了一些类似于首部有多长,载荷(payload)有多长,上层协议是什么等信息。
  • 数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,根据首部中的"上层协议字段"将数据交给对应的上层协议处理。

7. 网络中的地址管理

认识IP地址:

IP协议有两个版本,IPv4和IPv6。我们整个的课程,凡是提到IP协议,没有特殊说明的,默认都是指IPv4。

IP地址是在IP协议中,用来标识网络中不同主机的地址;对于IPv4来说,IP地址是一个4字节,32位的整数;我们通常也使用"点分十进制"的字符串表示IP地址,例如192.168.0.1;用点分割的每一个数字表示一个字节,范围是0-255。

认识MAC地址:

MAC地址用来识别数据链路层中相连的节点;长度为48位,即6个字节。一般用16进制数字加上冒号的形式来表示(例如:08:00:27:03:fb:19)。在网卡出厂时就确定了,不能修改。mac地址通常是唯一的(虚拟机中的mac地址不是真实的mac地址,可能会冲突;也有些网卡支持用户配置mac地址)。

MAC地址和IP地址的区别:

在进行网络传输的过程中始终都有两个问题:

  1. 从哪里来(起始地址),到哪里去(最终目的地址),为我们为了每一个阶段提供方向目标,方便路径选择,这个就是IP地址。
  2. 上一站从哪来的,下一站到哪里去,该类地址一直在变换,像这种相邻的两个地址在进行跳跃的地址是MAC地址。

协议报头:

协议的每一层都用,而每一层协议的最终表现就是协议要用报头。

类比现实我们在进行网络购物的时候,收到货物时一般来说收到的不只货物本身还有快递单,这里的快递单就是报头,所以在我们接收数据的时候往往会比发送时多一些数据。

报头的意义是:和快递单类似,报头里面的数据不是给我们看到而是给快递员看到,上面有发往的地址,因为只要是同一个快递公司那么快递单的格式就是一致的,这种保证一致性的约定就是协议。

所以协议通常是由报头来进行表达的。每一份数据最终在被发送或者在不同层协议中,都要有自己的报头。


二、网络编程基础

1. 理解源IP地址和目的IP地址

在IP数据包头部中,有两个IP地址,分别叫做源IP地址和目的IP地址。

思考:我们光有IP地址就可以完成通信了嘛?想象一下发qq消息的例子,有了IP地址能够把消息发送到对方的机器上,但是还需要有一个其他的标识来区分出,这个数据要给哪个程序进行解析。

2. 认识端口号

端口号(port)是传输层协议的内容:

  • 端口号是一个2字节16位的整数。
  • 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理。
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程。
  • 一个端口号只能被一个进程占用。

理解"端口号"和"进程ID":

我们之前在学习系统编程的时候,学习了pid表示唯一一个进程;此处我们的端口号也是唯一表示一个进程。那么这两者之间是怎样的关系?

另外,一个进程可以绑定多个端口号;但是一个端口号不能被多个进程绑定。

理解源端口号和目的端口号:

传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号。就是在描述"数据是谁发的,要发给谁"。

为什么有了pid还要使用port:

  1. 系统是系统,网络是网络,单独设置可以保证在pid改变的时候网络也不用跟着改变,我们要保证解耦性。
  2. 我们在进行通信的时候往往是客户端主动发送数据给服务端,所以我们要保证客户端要能找到服务端进程,所以要求服务器的唯一性不能改变,所以服务器的ip加端口不能改变。
  3. 不是所有的进程都需要port,但是每一个进程需要pid。

os如何根据port找到指定的进程:

我们知道进程的pcb是一个结构体,而port是一个数字,我们如何通过一个数字找到一个结构体?我们一般会采用hash的策略。

所以一个进程可以有多个端口号,但是一个端口号只能有一个进程。

所有我们在进行网络通信的过程中,除了我们要发送的数据之外我们也要发送自己的ip和port给对方,因为我们有可能还要发送回来。所以我们发送数据的时候一定会多发送一部分,这一部分数据会以协议的形式呈现。

3. 认识TCP协议

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

4. 认识UDP协议

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

5. 网络字节序

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

发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。

因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据;如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可。

将0x1234abcd写入到以0x0000开始的内存中:

  • big-endian:0x0000: 0x12, 0x0001: 0x23, 0x0002: 0xab, 0x0003: 0xcd
  • little-endian:0x0000: 0xcd, 0x0001: 0xab, 0x0002: 0x34, 0x0003: 0x12

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:

c 复制代码
#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);  // 主机32位字节序 → 网络大端字节序(long,32位,多用于IP)
uint16_t htons(uint16_t hostshort); // 主机16位字节序 → 网络大端字节序(short,16位,多用于端口)
uint32_t ntohl(uint32_t netlong);   // 网络32位大端字节序 → 主机字节序
uint16_t ntohs(uint16_t netshort);  // 网络16位大端字节序 → 主机字节序

这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。

例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。

如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

6. socket编程接口

socket常见API:

c 复制代码
// 创建socket文件描述符(TCP/UDP,客户端 + 服务器)
int socket(int domain, int type, int protocol);

// 绑定端口号(TCP/UDP,服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);

// 开始监听socket(TCP,服务器)
int listen(int socket, int backlog);

// 接收请求(TCP,服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);

// 建立连接(TCP,客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockaddr结构:

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同。

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址。
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
  • socket API可以用struct sockaddr *类型表示,在使用的时候需要强制转化成sockaddr_in;这样的好处是程序的通用性,可以接收IPv4、IPv6,以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。

in_addr结构:

c 复制代码
typedef uint32_t in_addr_t;
struct in_addr
{
    in_addr_t s_addr;
};

in_addr用来表示一个IPv4的IP地址。其实就是一个32位的整数。


三、UDP网络程序

1. 封装UdpSocket

cpp 复制代码
#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <cassert>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;

class UdpSocket {
public:
    UdpSocket() : fd_(-1) {}

    bool Socket() {
        fd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (fd_ < 0) {
            perror("socket");
            return false;
        }
        return true;
    }

    bool Close() {
        close(fd_);
        return true;
    }

    bool Bind(const std::string& ip, uint16_t port) {
        sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        addr.sin_port = htons(port);
        int ret = bind(fd_, (sockaddr*)&addr, sizeof(addr));
        if (ret < 0) {
            perror("bind");
            return false;
        }
        return true;
    }

    bool RecvFrom(std::string* buf, std::string* ip = NULL, uint16_t* port = NULL) {
        char tmp[1024 * 10] = {0};
        sockaddr_in peer;
        socklen_t len = sizeof(peer);
        ssize_t read_size = recvfrom(fd_, tmp, sizeof(tmp) - 1, 0, (sockaddr*)&peer, &len);
        if (read_size < 0) {
            perror("recvfrom");
            return false;
        }
        buf->assign(tmp, read_size);
        if (ip != NULL) {
            *ip = inet_ntoa(peer.sin_addr);
        }
        if (port != NULL) {
            *port = ntohs(peer.sin_port);
        }
        return true;
    }

    bool SendTo(const std::string& buf, const std::string& ip, uint16_t port) {
        sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        addr.sin_port = htons(port);
        ssize_t write_size = sendto(fd_, buf.data(), buf.size(), 0, (sockaddr*)&addr, sizeof(addr));
        if (write_size < 0) {
            perror("sendto");
            return false;
        }
        return true;
    }

private:
    int fd_;
};

该代码中的知识点:

  • socket(AF_INET, SOCK_DGRAM, 0):创建UDP套接字,AF_INET 表示IPv4,SOCK_DGRAM 表示用户数据报套接字,第三个参数0表示默认协议。
  • bind(fd_, (sockaddr*)&addr, sizeof(addr)):绑定IP和端口,sockaddr_in 需要强制转换为 sockaddr*。
  • htons(port):将端口号从主机字节序转为网络字节序。
  • inet_addr(ip.c_str()):将点分十进制IP字符串转为32位整数(网络字节序)。
  • recvfrom:接收数据,后两个参数是输出型参数,返回发送方的IP和端口。
  • inet_ntoa(peer.sin_addr):将网络字节序的IP转为点分十进制字符串。
  • ntohs(peer.sin_port):将网络字节序的端口转为主机字节序。
  • sendto:发送数据,后两个参数指定目标IP和端口。

2. 实现英译汉服务器

cpp 复制代码
#include "udp_server.hpp"
#include <unordered_map>
#include <iostream>

std::unordered_map<std::string, std::string> g_dict;

void Translate(const std::string& req, std::string& resp) {
    auto it = g_dict.find(req);
    if (it == g_dict.end()) {
        resp = "未查到!";
        return;
    }
    resp = it->second;
}

int main(int argc, char* argv[]) {
    if (argc != 3) {
        printf("Usage ./dict_server [ip] [port]\n");
        return 1;
    }

    // 1. 数据初始化
    g_dict.insert(std::make_pair("hello", "你好"));
    g_dict.insert(std::make_pair("world", "世界"));
    g_dict.insert(std::make_pair("c++", "最好的编程语言"));
    g_dict.insert(std::make_pair("bit", "特别NB"));

    // 2. 启动服务器
    UdpServer server;
    server.Start(argv[1], atoi(argv[2]), Translate);

    return 0;
}

该代码中的知识点:

  • std::unordered_map 存储字典,insert 插入键值对。
  • Translate 函数是回调函数,根据请求查找字典,填充响应。
  • 服务器启动时传入IP、端口和回调函数。

3. UDP通用客户端

cpp 复制代码
#pragma once
#include "udp_socket.hpp"

class UdpClient {
public:
    UdpClient(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {
        assert(sock_.Socket());
    }

    ~UdpClient() {
        sock_.Close();
    }

    bool RecvFrom(std::string* buf) {
        return sock_.RecvFrom(buf);
    }

    bool SendTo(const std::string& buf) {
        return sock_.SendTo(buf, ip_, port_);
    }

private:
    UdpSocket sock_;
    std::string ip_;
    uint16_t port_;
};

实现英译汉客户端:

cpp 复制代码
#include "udp_client.hpp"
#include <iostream>

int main(int argc, char* argv[]) {
    if (argc != 3) {
        printf("Usage ./dict_client [ip] [port]\n");
        return 1;
    }
    UdpClient client(argv[1], atoi(argv[2]));
    for (;;) {
        std::string word;
        std::cout << "请输入您要查的单词:";
        std::cin >> word;
        if (!std::cin) {
            std::cout << "Good Bye" << std::endl;
            break;
        }
        client.SendTo(word);
        std::string result;
        client.RecvFrom(&result);
        std::cout << word << " 意思是 " << result << std::endl;
    }
    return 0;
}

4. 地址转换函数

本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位的IP地址。但是我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示和in_addr表示之间转换:

字符串转in_addr的函数:

c 复制代码
#include <arpa/inet.h>
int inet_aton(const char *strptr, struct in_addr *addrptr);
in_addr_t inet_addr(const char *strptr);
int inet_pton(int family, const char *strptr, void *addrptr);

in_addr转字符串的函数:

c 复制代码
char *inet_ntoa(struct in_addr inaddr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。

关于inet_ntoa:

inet_ntoa这个函数返回了一个char*,很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果。那么是否需要调用者手动释放呢?

因为inet_ntoa把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖掉上一次的结果。

思考:如果有多个线程调用inet_ntoa,是否会出现异常情况呢?在APUE中,明确提出inet_ntoa不是线程安全的函数;但是在centos7上测试,并没有出现问题,可能内部的实现加了互斥锁;同学们课后自己写程序验证一下在自己的机器上inet_ntoa是否会出现多线程的问题;在多线程环境下,推荐使用inet_ntop,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题。

5. UDP群聊

用户管理模块:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <unordered_map>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

using namespace std;

class User
{
public:
    User(const string &ip, const uint16_t &port) : _ip(ip), _port(port)
    {}
    ~User()
    {}
    string ip(){ return _ip; }
    uint16_t port(){ return _port; }
private:
    string _ip;
    uint16_t _port;
};

class OnlineUser
{
public:
    OnlineUser() {}
    ~OnlineUser() {}
    void addUser(const string &ip, const uint16_t &port)
    {
        string id = ip + "-" + to_string(port);
        users.insert(make_pair(id, User(ip, port)));
    }
    void delUser(const string &ip, const uint16_t &port)
    {
        string id = ip + "-" + to_string(port);
        users.erase(id);
    }
    bool isOnline(const string &ip, const uint16_t &port)
    {
        string id = ip + "-" + to_string(port);
        return users.find(id) == users.end() ? false : true;
    }
    void broadcastMessage(int sockfd, const string &ip, const uint16_t &port, const string &message)
    {
        // 给管理列表的所有人挨个发消息
        for (auto &user : users)
        {
            struct sockaddr_in client;
            bzero(&client, sizeof(client));

            client.sin_family = AF_INET;
            client.sin_port = htons(user.second.port());
            client.sin_addr.s_addr = inet_addr(user.second.ip().c_str());
            string s = ip + "-" + to_string(port) + "# ";
            s += message;
            sendto(sockfd, s.c_str(), s.size(), 0, (struct sockaddr *)&client, sizeof(client));
        }
    }

private:
    // 用ip+port来标定唯一的用户
    unordered_map<string, User> users;
};

该代码中的知识点:

  • User 类封装了用户的IP和端口。
  • OnlineUser 类用 unordered_map 管理在线用户,key是 ip-port 字符串。
  • addUser、delUser、isOnline 分别用于添加、删除、判断用户是否在线。
  • broadcastMessage 遍历所有在线用户,向每个人发送消息。

服务端:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <vector>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <functional>

namespace Server
{
    using namespace std;
    const static string _defaultIp = "0.0.0.0";  
    const static int g_num = 1024;

    enum {USAGE_ERR = 1,SOCKET_ERR = 2,BIND_ERR = 3,OPEN_ERR = 4};

    typedef function<void (int,string,uint16_t,string)> func_t;

    class udpServer
    {
    public:
        udpServer(const func_t& cb,uint16_t& port,const string& ip = _defaultIp)
        :_callback(cb)
        ,_port(port)
        ,_ip(ip)
        ,_sockfd(-1)
        {}

        void initServer()
        {
            _sockfd = socket(AF_INET,SOCK_DGRAM, 0);
            if(_sockfd == -1)
            {
                cout<<"socket err: " << errno << ": " << strerror(errno) <<endl;
                exit(SOCKET_ERR); 
            }
            cout << "socket success: " << " : " << _sockfd << endl;

            struct sockaddr_in local;
            bzero(&local,sizeof local);

            local.sin_family = AF_INET;
            local.sin_port = htons(_port);
            local.sin_addr.s_addr = inet_addr(_ip.c_str());

            int n = bind(_sockfd,(struct sockaddr*)&local,sizeof local);
            if(n==-1)
            {
                cout<<"bind err: " << errno << ": " << strerror(errno) <<endl;
                exit(BIND_ERR); 
            }
        }

        void start()
        {
            char buffer[g_num];
            
            for(;;)
            {
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                ssize_t s = recvfrom(_sockfd,buffer,sizeof (buffer) - 1, 0 ,(struct sockaddr *)&peer , &len);
                if(s > 0)
                {
                    buffer[s] = 0;
                    string clientip = inet_ntoa(peer.sin_addr);
                    uint16_t clientport = ntohs(peer.sin_port);
                    string message = buffer;

                    cout << clientip <<"[" << clientport << "]# " << message << endl;

                    _callback(_sockfd,clientip,clientport,message);
                }
            }
        }

        ~udpServer()
        {
        }

    private:
        uint16_t _port;
        string _ip;
        int _sockfd;

        func_t _callback;
    };
}

该代码中的知识点:

  • func_t 是回调函数类型,参数为 sockfd、clientip、clientport、message。
  • initServer 创建socket、绑定IP和端口。
  • start 是死循环,不断接收数据,调用回调函数。
  • recvfrom 的 peer 和 len 是输出型参数,返回发送方的IP和端口。
  • inet_ntoa 将网络字节序的IP转为点分十进制字符串。
  • ntohs 将网络字节序的端口转为主机字节序。

服务端主程序:

cpp 复制代码
#include "UdpServer.hpp"
#include "onlineUser.hpp"
#include <memory>
#include <fstream>
#include <unordered_map>
#include <signal.h>

using namespace std;
using namespace Server;

static void Usage(string proc)
{
    cerr << "\nUsage:\n\t" << proc << " local_port\n\n";
}

OnlineUser onlineuser;

void routeMessage(int sockfd, string clientip, uint16_t clientport, string message)
{
    if (message == "online") onlineuser.addUser(clientip, clientport);
    if (message == "offline") onlineuser.delUser(clientip, clientport);
    if (onlineuser.isOnline(clientip, clientport))
    {
        onlineuser.broadcastMessage(sockfd, clientip, clientport, message);
    }
    else
    {
        struct sockaddr_in client;
        bzero(&client, sizeof(client));

        client.sin_family = AF_INET;
        client.sin_port = htons(clientport);
        client.sin_addr.s_addr = inet_addr(clientip.c_str());

        string response = "你还没有上线,请先上线,请运行: online";

        sendto(sockfd, response.c_str(), response.size(), 0, (struct sockaddr *)&client, sizeof(client));
    }
}

// ./udpServer port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<udpServer> usvr(new udpServer(routeMessage, port));

    usvr->initServer();
    usvr->start();

    return 0;
}

该代码中的知识点:

  • routeMessage 是回调函数,根据消息内容决定是否添加/删除用户,或者广播消息。
  • 如果用户未上线,单独发送提醒消息。
  • sendto 向指定客户端发送消息。

客户端:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <strings.h>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>

namespace Client
{
    using namespace std;

    class udpClient
    {
    public:
        udpClient(const string &serverip, const uint16_t &serverport)
            : _serverip(serverip), _serverport(serverport), _sockfd(-1), _quit(false)
        {
        }
        void initClient()
        {
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (_sockfd == -1)
            {
                cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
                exit(2);
            }
            cout << "socket success: " << " : " << _sockfd << endl;
        }

        static void* readMessage(void* argcs)
        {
            int sockfd = *(static_cast<int*> (argcs));
            pthread_detach(pthread_self());
            while (true)
            {
                char buffer[1024];
                struct sockaddr_in tmp;
                socklen_t tmp_len = sizeof(tmp);
                size_t n = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr *)&tmp, &tmp_len);
                 
                if(n>0) 
                    buffer[n] = 0;

                cout << buffer << endl;
            }
            return nullptr;
        }

        void run()
        {
            pthread_create(&_reader, nullptr, readMessage, (void*)&_sockfd);

            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_addr.s_addr = inet_addr(_serverip.c_str());
            server.sin_port = htons(_serverport);

            string message;
            char cmdline[1024];

            while (!_quit)
            {
                fprintf(stderr, "Enter# ");
                fflush(stderr);
                fgets(cmdline, sizeof(cmdline), stdin);
                cmdline[strlen(cmdline)-1] = 0;
                message = cmdline;
                sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));          
            }
        }
        ~udpClient()
        {
        }

    private:
        int _sockfd;
        string _serverip;
        uint16_t _serverport;
        bool _quit;

        pthread_t _reader;
    };
}

该代码中的知识点:

  • readMessage 是静态成员函数,因为 pthread_create 要求回调函数是 void* (*)(void*) 类型,普通成员函数有隐藏的 this 指针,无法匹配。
  • pthread_detach(pthread_self()):线程自己分离自己,退出时自动释放资源。
  • pthread_create 创建一个读线程,专门接收服务端消息。
  • sendto 发送消息给服务端。
  • 主循环读取用户输入,发送消息。

四、TCP网络程序

1. TCP socket API详解

socket():

c 复制代码
int socket(int domain, int type, int protocol);
  • socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符。
  • 应用程序可以像读写文件一样用read/write在网络上收发数据。
  • 如果socket()调用出错则返回-1。
  • 对于IPv4,family参数指定为AF_INET。
  • 对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。
  • protocol参数的介绍从略,指定为0即可。

bind():

服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接;服务器需要调用bind绑定一个固定的网络地址和端口号。

  • bind()成功返回0,失败返回-1。
  • bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。
  • struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。

我们的程序中对myaddr参数是这样初始化的:

c 复制代码
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
  1. 将整个结构体清零。
  2. 设置地址类型为AF_INET。
  3. 网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址。
  4. 端口号为SERV_PORT,我们定义为9999。

listen():

c 复制代码
int listen(int sockfd, int backlog);
  • listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略,这里设置不会太大(一般是5)。
  • listen()成功返回0,失败返回-1。

accept():

c 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 三次握手完成后,服务器调用accept()接受连接。
  • 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。
  • addr是一个传出参数,accept()返回时传出客户端的地址和端口号。
  • 如果给addr参数传NULL,表示不关心客户端的地址。
  • addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。

我们的服务器程序结构是这样的:

c 复制代码
while (1) {
    cliaddr_len = sizeof(cliaddr);
    connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
    n = read(connfd, buf, MAXLINE);
    ...
    close(connfd);
}

理解accept的返回值:饭店拉客例子

connect():

c 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 客户端需要调用connect()连接服务器。
  • connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。
  • connect成功返回0,出错返回-1。

2. 封装TCP socket

cpp 复制代码
#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <string>
#include <cassert>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>

typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;

#define CHECK_RET(exp) if (!(exp)) {\
    return false;\
}

class TcpSocket {
public:
    TcpSocket() : fd_(-1) {  }
    TcpSocket(int fd) : fd_(fd) {  }

    bool Socket() {
        fd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (fd_ < 0) {
            perror("socket");
            return false;
        }
        printf("open fd = %d\n", fd_);
        return true;
    }

    bool Close() const {
        close(fd_);
        printf("close fd = %d\n", fd_);
        return true;
    }

    bool Bind(const std::string& ip, uint16_t port) const {
        sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        addr.sin_port = htons(port);
        int ret = bind(fd_, (sockaddr*)&addr, sizeof(addr));
        if (ret < 0) {
            perror("bind");
            return false;
        }
        return true;
    }

    bool Listen(int num) const {
        int ret = listen(fd_, num);
        if (ret < 0) {
            perror("listen");
            return false;
        }
        return true;
    }

    bool Accept(TcpSocket* peer, std::string* ip = NULL, uint16_t* port = NULL) const {
        sockaddr_in peer_addr;
        socklen_t len = sizeof(peer_addr);
        int new_sock = accept(fd_, (sockaddr*)&peer_addr, &len);
        if (new_sock < 0) {
            perror("accept");
            return false;
        }
        printf("accept fd = %d\n", new_sock);
        peer->fd_ = new_sock;
        if (ip != NULL) {
            *ip = inet_ntoa(peer_addr.sin_addr);
        }
        if (port != NULL) {
            *port = ntohs(peer_addr.sin_port);
        }
        return true;
    }

    bool Recv(std::string* buf) const {
        buf->clear();
        char tmp[1024 * 10] = {0};
        // [注意!] 这里的读并不算很严谨,因为一次 recv 并不能保证把所有的数据都全部读完
        // 参考 man 手册 MSG_WAITALL 节。
        ssize_t read_size = recv(fd_, tmp, sizeof(tmp), 0);
        if (read_size < 0) {
            perror("recv");
            return false;
        }
        if (read_size == 0) {
            return false;
        }
        buf->assign(tmp, read_size);
        return true;
    }

    bool Send(const std::string& buf) const {
        ssize_t write_size = send(fd_, buf.data(), buf.size(), 0);
        if (write_size < 0) {
            perror("send");
            return false;
        }
        return true;
    }

    bool Connect(const std::string& ip, uint16_t port) const {
        sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        addr.sin_port = htons(port);
        int ret = connect(fd_, (sockaddr*)&addr, sizeof(addr));
        if (ret < 0) {
            perror("connect");
            return false;
        }
        return true;
    }

    int GetFd() const {
        return fd_;
    }

private:
    int fd_;
};

该代码中的知识点:

  • socket(AF_INET, SOCK_STREAM, 0):创建TCP套接字,SOCK_STREAM 表示面向流的传输协议。
  • Bind:绑定IP和端口,服务器需要显式bind。
  • Listen:将socket设置为监听状态,backlog 指定连接队列长度。
  • Accept:接受连接,返回新的socket用于和客户端通信。
  • Recv:接收数据,recv 返回0表示对端关闭连接。
  • Send:发送数据。
  • Connect:客户端发起连接。

3. TCP通用服务器

cpp 复制代码
#pragma once
#include <functional>
#include "tcp_socket.hpp"

typedef std::function<void(const std::string& req, std::string* resp)> Handler;

class TcpServer {
public:
    TcpServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) { }

    bool Start(Handler handler) {
        // 1.创建socket;
        CHECK_RET(listen_sock_.Socket());
        // 2.绑定端口号
        CHECK_RET(listen_sock_.Bind(ip_, port_));
        // 3.进行监听
        CHECK_RET(listen_sock_.Listen(5));
        // 4.进入事件循环
        for (;;) {
            // 5.进行accept
            TcpSocket new_sock;
            std::string ip;
            uint16_t port = 0;
            if (!listen_sock_.Accept(&new_sock, &ip, &port)) {
                continue;
            }
            printf("[client %s:%d] connect!\n", ip.c_str(), port);
            // 6.进行循环读写
            for (;;) {
                std::string req;
                // 7.读取请求,读取失败则结束循环
                bool ret = new_sock.Recv(&req);
                if (!ret) {
                    printf("[client %s:%d] disconnect!\n", ip.c_str(), port);
                    // [注意!] 需要关闭socket
                    new_sock.Close();
                    break;
                }
                // 8.计算响应
                std::string resp;
                handler(req, &resp);
                // 9.写回响应
                new_sock.Send(resp);
            }
        }
        return true;
    }

private:
    TcpSocket listen_sock_;
    std::string ip_;
    uint64_t port_;
};

该代码中的知识点:

  • Handler 是回调函数类型,处理请求并填充响应。
  • Start 是服务器主循环,先创建socket、bind、listen,然后进入accept循环。
  • 每次accept到一个新连接,就进入内层循环,不断读取请求、处理、发送响应。
  • Recv 返回false表示对端关闭连接,此时需要关闭socket并跳出内层循环。
  • 这种单连接版本只能同时处理一个客户端,因为内层循环会一直阻塞在read上。

TCP客户端:

cpp 复制代码
#pragma once
#include "tcp_socket.hpp"

class TcpClient {
public:
    TcpClient(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {
        // [注意!!] 需要先创建好 socket
        sock_.Socket();
    }

    ~TcpClient() {
        sock_.Close();
    }

    bool Connect() {
        return sock_.Connect(ip_, port_);
    }

    bool Recv(std::string* buf) {
        return sock_.Recv(buf);
    }

    bool Send(const std::string& buf) {
        return sock_.Send(buf);
    }

private:
    TcpSocket sock_;
    std::string ip_;
    uint16_t port_;
};

该代码中的知识点:

  • 客户端在构造时创建socket。
  • Connect 发起连接请求。
  • 客户端不需要bind,操作系统会自动分配端口。

4. TCP多进程版本

cpp 复制代码
#pragma once
#include <functional>
#include <signal.h>
#include "tcp_socket.hpp"

typedef std::function<void (const std::string& req, std::string* resp)> Handler;

class TcpProcessServer {
public:
    TcpProcessServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {
        // 需要处理子进程
        signal(SIGCHLD, SIG_IGN);
    }

    void ProcessConnect(const TcpSocket& new_sock, const std::string& ip, uint16_t port, Handler handler) {
        for (;;) {
            std::string req;
            bool ret = new_sock.Recv(&req);
            if (!ret) {
                printf("[client %s:%d] disconnected!\n", ip.c_str(), port);
                break;
            }
            std::string resp;
            handler(req, &resp);
            new_sock.Send(resp);
            printf("[client %s:%d] req: %s, resp: %s\n", ip.c_str(), port, req.c_str(), resp.c_str());
        }
    }

    bool Start(Handler handler) {
        // 1.创建socket;
        CHECK_RET(listen_sock_.Socket());
        // 2.绑定端口号
        CHECK_RET(listen_sock_.Bind(ip_, port_));
        // 3.进行监听
        CHECK_RET(listen_sock_.Listen(5));
        // 4.进入循环
        for (;;) {
            // 5.进行accept
            TcpSocket new_sock;
            std::string ip;
            uint16_t port = 0;
            if (!listen_sock_.Accept(&new_sock, &ip, &port)) {
                continue;
            }
            printf("[client %s:%d] connect!\n", ip.c_str(), port);
            // 6.创建子进程
            pid_t id = fork();
            if (id == 0) {
                // 子进程
                close(listen_sock_.GetFd());
                ProcessConnect(new_sock, ip, port, handler);
                new_sock.Close();
                exit(0);
            }
            // 父进程
            new_sock.Close();
        }
        return true;
    }

private:
    TcpSocket listen_sock_;
    std::string ip_;
    uint64_t port_;
};

该代码中的知识点:

  • signal(SIGCHLD, SIG_IGN):忽略SIGCHLD信号,子进程退出时自动回收,不会产生僵尸进程。
  • 每次accept后,fork子进程处理连接,父进程继续accept。
  • 子进程关闭监听socket,父进程关闭已连接socket。
  • 子进程处理完连接后exit,由内核自动回收。

5. TCP多线程版本

cpp 复制代码
#pragma once
#include <functional>
#include <pthread.h>
#include "tcp_socket.hpp"

typedef std::function<void (const std::string&, std::string*) > Handler;

struct ThreadArg {
    TcpSocket new_sock;
    std::string ip;
    uint16_t port;
    Handler handler;
};

class TcpThreadServer {
public:
    TcpThreadServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) { }

    bool Start(Handler handler) {
        // 1.创建 socket;
        CHECK_RET(listen_sock_.Socket());
        // 2.绑定端口号
        CHECK_RET(listen_sock_.Bind(ip_, port_));
        // 3.进行监听
        CHECK_RET(listen_sock_.Listen(5));
        // 4.进入循环
        for (;;) {
            // 5.进行 accept
            ThreadArg* arg = new ThreadArg();
            arg->handler = handler;
            bool ret = listen_sock_.Accept(&arg->new_sock, &arg->ip, &arg->port);
            if (!ret) {
                continue;
            }
            printf("[client %s:%d] connect\n", arg->ip.c_str(), arg->port);
            // 6. 创建新的线程完成具体操作
            pthread_t tid;
            pthread_create(&tid, NULL, ThreadEntry, arg);
            pthread_detach(tid);
        }
        return true;
    }

    // 这里的成员函数为啥非得是 static?
    static void* ThreadEntry(void* arg) {
        ThreadArg* p = reinterpret_cast<ThreadArg*>(arg);
        ProcessConnect(p);
        // 一定要记得释放内存!!!也要记得关闭文件描述符
        p->new_sock.Close();
        delete p;
        return NULL;
    }

    // 处理单次连接。这个函数也得是 static
    static void ProcessConnect(ThreadArg* arg) {
        // 1. 循环进行读写
        for (;;) {
            std::string req;
            // 2. 读取请求
            bool ret = arg->new_sock.Recv(&req);
            if (!ret) {
                printf("[client %s:%d] disconnected!\n", arg->ip.c_str(), arg->port);
                break;
            }
            std::string resp;
            // 3. 根据请求计算响应
            arg->handler(req, &resp);
            // 4. 发送响应
            arg->new_sock.Send(resp);
            printf("[client %s:%d] req: %s, resp: %s\n", arg->ip.c_str(), arg->port, req.c_str(), resp.c_str());
        }
    }

private:
    TcpSocket listen_sock_;
    std::string ip_;
    uint16_t port_;
};

该代码中的知识点:

  • ThreadArg 结构体封装了新socket、IP、端口和回调函数。
  • ThreadEntry 必须是static,因为 pthread_create 要求回调函数是 void* (*)(void*) 类型。
  • pthread_detach(tid):线程分离,退出时自动回收资源。
  • ProcessConnect 处理连接,循环读写。
  • 注意释放内存和关闭文件描述符。

6. 线程池版本TCP服务器

ThreadPool.hpp:

cpp 复制代码
#pragma once

#include "Thread.hpp"
#include "LockGuard.hpp"
#include "log.hpp"
#include <vector>
#include <queue>
#include <mutex>
#include <pthread.h>
#include <unistd.h>

using namespace ThreadNs;

const int gnum = 10;

template <class T>
class ThreadPool;

template <class T>
class ThreadData
{
public:
    ThreadPool<T> *threadpool;
    std::string name;

public:
    ThreadData(ThreadPool<T> *tp, const std::string &n) : threadpool(tp), name(n)
    {
    }
};

template <class T>
class ThreadPool
{
private:
    static void *handlerTask(void *args)
    {
        ThreadData<T> *td = (ThreadData<T> *)args;
        while (true)
        {
            T t;
            {
                LockGuard lockguard(td->threadpool->mutex());
                while (td->threadpool->isQueueEmpty())
                {
                    td->threadpool->threadWait();
                }
                t = td->threadpool->pop(); // pop的本质,是将任务从公共队列中,拿到当前线程自己独立的栈中
            }
            t(); //执行任务 回调函数
        }
        delete td;
        return nullptr;
    }

    ThreadPool(const int &num = gnum) : _num(num)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
        for (int i = 0; i < _num; i++)
        {
            _threads.push_back(new Thread());
        }
    }

    void operator=(const ThreadPool &) = delete;
    ThreadPool(const ThreadPool &) = delete;

public:
    void lockQueue() { pthread_mutex_lock(&_mutex); }
    void unlockQueue() { pthread_mutex_unlock(&_mutex); }
    bool isQueueEmpty() { return _task_queue.empty(); }
    void threadWait() { pthread_cond_wait(&_cond, &_mutex); }
    T pop()
    {
        T t = _task_queue.front();
        _task_queue.pop();
        return t;
    }
    pthread_mutex_t *mutex()
    {
        return &_mutex;
    }

public:
    void run()
    {
        for (const auto &t : _threads)
        {
            ThreadData<T> *td = new ThreadData<T>(this, t->threadname());
            t->start(handlerTask, td);
            logMessage(DEBUG, "%s start ...", t->threadname().c_str());
        }
    }
    void push(const T &in)
    {
        LockGuard lockguard(&_mutex);
        _task_queue.push(in);
        pthread_cond_signal(&_cond);
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
        for (const auto &t : _threads)
            delete t;
    }

    static ThreadPool<T> *getInstance()
    {
        if (nullptr == tp)
        {
            _singlock.lock();
            if (nullptr == tp)
            {
                tp = new ThreadPool<T>();
            }
            _singlock.unlock();
        }
        return tp;
    }

private:
    int _num;
    std::vector<Thread *> _threads;
    std::queue<T> _task_queue;
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;

    static ThreadPool<T> *tp;
    static std::mutex _singlock;
};

template <class T>
ThreadPool<T> *ThreadPool<T>::tp = nullptr;

template <class T>
std::mutex ThreadPool<T>::_singlock;

该代码中的知识点:

  • handlerTask 是static,因为 pthread_create 要求回调函数是 void* (*)(void*) 类型。
  • LockGuard 在构造函数中加锁,在析构函数中解锁。
  • while (td->threadpool->isQueueEmpty()) 循环等待,避免虚假唤醒。
  • t() 处理任务放到锁的外面,因为pop的本质是将任务从公共队列中拿到当前线程自己独立的栈中,此时数据不再是临界资源。
  • getInstance 是线程安全的单例模式,双重检查锁。

Task.hpp:

cpp 复制代码
#pragma once
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <iostream>
#include <string>
#include <cstdio>
#include <functional>
#include "log.hpp"

void serviceIO(int sock)
{
    char buffer[1024];
    while (true)
    {
        ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "recv message: " << buffer << std::endl;

            std::string outbuffer = buffer;
            outbuffer += " server[echo]";

            write(sock, outbuffer.c_str(), outbuffer.size());
        }
        else if (n == 0)
        {
            logMessage(NORMAL, "client quit, me too!");
            break;
        }
    }
    close(sock);
}

class Task
{
    using func_t = std::function<void(int)>;

public:
    Task()
    {
    }
    Task(int sock, func_t func)
        : _sock(sock), _callback(func)
    {
    }
    void operator()()
    {
        _callback(_sock);
    }

private:
    int _sock;
    func_t _callback;
};

该代码中的知识点:

  • Task 类封装了socket和回调函数。
  • operator() 执行任务,调用回调函数。
  • serviceIO 是具体的业务处理函数,循环读取数据并回显。

7. 守护进程

关闭Xshell导致服务器停止,本质是因为该服务器进程仍属于与Xshell伪终端绑定的会话之中,当终端设备销毁时,内核会向该会话内的所有进程发送SIGHUP信号致其默认退出,而作业控制中的前台或后台(bg)状态仅改变运行模式,并未改变进程归属的会话和终端关联;守护进程化的核心原理正是通过调用setsid创建新会话并脱离原终端,使进程自成会话、自成进程组且彻底独立于任何终端设备,从而隔绝终端生命周期的影响,这样即便用户注销或关闭Xshell,服务器进程也不会收到终止信号,最终实现不受登陆注销影响的常驻运行。

cpp 复制代码
#pragma once

#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define DEV "/dev/null"

void daemonSelf(const char *currPath = nullptr)
{
    // 1. 让调用进程忽略掉异常的信号
    signal(SIGPIPE, SIG_IGN);

    // 2. 如何让自己不是组长,setsid
    if (fork() > 0)
        exit(0);
    // 子进程 -- 守护进程,精灵进程,本质就是孤儿进程的一种!
    pid_t n = setsid();
    // 自己创建一个会话并且把自己id作为进程组的组长
    // 只能是一个普通的进程可以成为守护进程 组长不能成为守护进程
    // setsid调用成功会创建新会话 调用进程成为新会话首进程 新进程组的组长 并脱离原控制终端
    // 进程组组长调用setsid会失败 返回-1 因此创建守护进程时需先确保进程非组长(如fork子进程)
    // 非组长调用setsid会把自己改为组长

    assert(n != -1);

    // 3. 守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件
    int fd = open(DEV, O_RDWR);
    if(fd >= 0)
    {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);

        close(fd);
    }
    else
    {
        close(0);
        close(1);
        close(2);
    }

    // 4. 可选:进程执行路径发生更改
    if(currPath) chdir(currPath);
}

该代码中的知识点:

  • signal(SIGPIPE, SIG_IGN):忽略SIGPIPE信号,防止客户端异常断开导致进程崩溃。
  • fork() 后父进程退出,子进程成为孤儿进程,不再是进程组组长。
  • setsid():创建新会话,子进程成为会话首进程和进程组组长,脱离原控制终端。
  • dup2(fd, 0/1/2):将标准输入、标准输出、标准错误重定向到 /dev/null。
  • /dev/null 是一个黑洞设备,写入的数据被丢弃,读取不到任何数据。
  • chdir(currPath):更改工作目录,防止占用挂载点。

守护进程化的核心原理:

守护进程化的核心原理在于通过一系列系统调用彻底切断进程与用户终端会话之间的关联,使其成为独立于任何登陆会话而运行的"孤儿进程"。具体实现时,首先通过 signal(SIGPIPE, SIG_IGN) 忽略TCP通信中可能出现的SIGPIPE信号,防止因客户端异常断开而导致进程崩溃;接着调用 fork() 创建子进程并让父进程直接退出,使子进程不再是进程组组长,从而满足 setsid() 的调用条件------该调用会创建一个全新的会话,使子进程成为该会话的首进程和新进程组的组长,同时彻底脱离原控制终端(即Xshell所关联的伪终端),这样当用户关闭Xshell导致终端设备销毁时,内核只会向原会话发送SIGHUP信号,而新会话因与终端无任何绑定关系,其内的守护进程不会收到该终止信号;此后,守护进程将标准输入、标准输出和标准错误重定向到 /dev/null 这个"黑洞"设备,既避免了因操作已失效的终端文件描述符而引发的错误,也确保了进程在无终端环境下不会因读写操作而阻塞或崩溃;最后可选地通过 chdir() 将工作目录切换到根目录或指定路径,防止进程因占用挂载点而影响文件系统卸载。整个流程使服务器进程彻底脱离用户登陆会话的生命周期制约,从而实现了7x24小时不受用户注销影响的常驻运行。


五、TCP通信流程和协议定制

1. TCP通信流程

服务器初始化:

  • 调用socket,创建文件描述符。
  • 调用bind,将当前的文件描述符和ip/port绑定在一起;如果这个端口已经被其他进程占用了,就会bind失败。
  • 调用listen,声明当前这个文件描述符作为一个服务器的文件描述符,为后面的accept做好准备。
  • 调用accept,并阻塞,等待客户端连接过来。

建立连接的过程:

  • 调用socket,创建文件描述符。
  • 调用connect,向服务器发起连接请求。
  • connect会发出SYN段并阻塞等待服务器应答;(第一次)
  • 服务器收到客户端的SYN,会应答一个SYN-ACK段表示"同意建立连接";(第二次)
  • 客户端收到SYN-ACK后会从connect()返回,同时应答一个ACK段;(第三次)

这个建立连接的过程,通常称为三次握手。

断开连接的过程:

  • 如果客户端没有更多的请求了,就调用close()关闭连接,客户端会向服务器发送FIN段(第一次)。
  • 此时服务器收到FIN后,会回应一个ACK,同时read会返回0(第二次)。
  • read返回之后,服务器就知道客户端关闭了连接,也调用close关闭连接,这个时候服务器会向客户端发送一个FIN;(第三次)
  • 客户端收到FIN,再返回一个ACK给服务器;(第四次)

这个断开连接的过程,通常称为四次挥手。

断开连接为什么是四次?建立连接的时候是有一方主动的,但是在断开连接的时候就不在是一方的事情了而是双方的事了,所以就要争得双方的同意。

2. 协议定制

协议是一种"约定"。socket api的接口,在读写数据时,都是按"字符串"的方式来发送接收的。如果我们要传输一些"结构化的数据"怎么办呢?

我们之前的代码中我们是直接通过文件的操作往fd里面读数据的,但是我们没法保证每一次读都是读完整的,我们只是里面有多少就读多少,所以就有可能导致我读的数据可能是几条别人不同人发的数据的混合。假设里面的数据是姓名班级之类的有对应关系的数据,混合了就分不开了,所以我们要杜绝这种现象。

所以说我们要把是一起的数据打包成一个报文(可以简单理解成打包成一个长字符串)。所以我们把多个数据变成一个数据的过程叫做序列化,所以我们把一堆数据序列化成一个数据在发送就不会有混合的情况了。我们把序列化的数据再拆分为多个数据的过程叫做反序列化。

所以业务结构化的数据发送到网络中的时候要先序列化再发送,收到的一定就是序列字节流,要先进性反序列化才能使用,这种就叫做业务协议。

接口协议定制:

就是未来我怎么读和返回的时候怎么返回,分为自己定制和使用函数定制,使用条件编译进行区分。

  1. 自己写
  2. 用现成的,对于序列化和反序列化有现成的解决方案
    • json,我们使用这个简单
    • protobuf
cpp 复制代码
#pragma once

#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>

#define SEP " "
#define SEP_LEN strlen(SEP)
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP)

enum
{
    OK = 0,
    DIV_ZERO,
    MOD_ZERO,
    OP_ERROR
};

// "x op y" -> "content_len"\r\n"x op y"\r\n
// "exitcode result" -> "content_len"\r\n"exitcode result"\r\n
// 这里\r\n没有声明特殊含义只是一个分割符
// 我们怎么保证content_len读完了只要读到\r\n我们就认为读完了 就是整个字符串由分隔符来保证
// exitcode result 是由content_len保证的
std::string enLength(const std::string &text) //添加
{
    std::string send_string = std::to_string(text.size());
    send_string += LINE_SEP;
    send_string += text;
    send_string += LINE_SEP;

    return send_string;
}

// "content_len"\r\n"exitcode result"\r\n
bool deLength(const std::string &package, std::string *text)
{
    auto pos = package.find(LINE_SEP);
    if (pos == std::string::npos)
        return false;
    std::string text_len_string = package.substr(0, pos);
    int text_len = std::stoi(text_len_string);
    *text = package.substr(pos + LINE_SEP_LEN, text_len);
    return true;
}

class Request
{
public:
    Request() : x(0), y(0), op(0)
    {
    }
    Request(int x_, int y_, char op_) : x(x_), y(y_), op(op_)
    {
    }

    bool serialize(std::string* out)
    {
#ifdef MYSELF
        *out = "";
        std::string x_string = std::to_string(x);
        std::string y_string = std::to_string(y);

        *out = x_string;
        *out += SEP;
        *out += op;
        *out += SEP;
        *out += y_string;

#else
        Json::Value root;
        root["first"] = x;
        root["second"] = y;
        root["oper"] = op;

        Json::FastWriter writer;

        *out = writer.write(root);
#endif 
        return true;
    }

    bool deserialize(const std::string &in)
    {
#ifdef MYSELF
        auto left = in.find(SEP);
        auto right = in.rfind(SEP);
        if (left == std::string::npos || right == std::string::npos)
            return false;
        if (left == right)
            return false;
        if (right - (left + SEP_LEN) != 1)
            return false;

        std::string x_string = in.substr(0, left);
        std::string y_string = in.substr(right + SEP_LEN);

        if (x_string.empty())
            return false;
        if (y_string.empty())
            return false;
        x = std::stoi(x_string);
        y = std::stoi(y_string);

        op = in[left + SEP_LEN];

#else
    Json::Value root;
    Json::Reader reader;

    reader.parse(in, root);

    x = root["first"].asInt();
    y = root["second"].asInt();
    op = root["oper"].asInt();

#endif 

        return true;
    }

public:
    int x;
    int y;
    char op;
};

class Response
{
public:
    Response() : exitcode(0), result(0)
    {
    }
    Response(int exitcode_, int result_) : exitcode(exitcode_), result(result_)
    {
    }
    bool serialize(std::string* out)
    {
#ifdef MYSELF
        *out = "";
        std::string ec_string = std::to_string(exitcode);
        std::string res_string = std::to_string(result);

        *out = ec_string;
        *out += SEP;
        *out += res_string;
#else
        Json::Value root;
        root["exitcode"] = exitcode;
        root["result"] = result;

        Json::FastWriter writer;
        *out = writer.write(root);

#endif 
        return true;
    }
    bool deserialize(const std::string &in)
    {
#ifdef MYSELF
        auto mid = in.find(SEP);
        if (mid == std::string::npos)
            return false;

        std::string ec_string = in.substr(0, mid);
        std::string res_string = in.substr(mid + SEP_LEN);
        if (ec_string.empty() || res_string.empty())
            return false;

        exitcode = std::stoi(ec_string);
        result = std::stoi(res_string);
#else
        Json::Value root;
        Json::Reader reader;
        reader.parse(in, root);
        
        exitcode = root["exitcode"].asInt();
        result = root["result"].asInt();
#endif 
        return true;
    }

public:
    int exitcode;
    int result;
};

// "content_len"\r\n"x op y"\r\n"content_len"\r\n"x op y"\r\n"content_len"\r\n"x op
bool recvPackage(int sock, std::string &inbuffer, std::string *text)
{
    char buffer[1024];
    while (true)
    {
        ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            buffer[n] = 0;
            inbuffer += buffer;
            auto pos = inbuffer.find(LINE_SEP);
            if (pos == std::string::npos)
                continue;
            
            std::string text_len_string = inbuffer.substr(0, pos);
            int text_len = std::stoi(text_len_string);

            int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;
            std::cout << "处理前#inbuffer: \n" << inbuffer << std::endl;
            if (inbuffer.size() < total_len)
            {
                std::cout << "你输入的消息,没有严格遵守我们的协议,正在等待后续的内容, continue" << std::endl;
                continue;
            }

            *text = inbuffer.substr(0, total_len);
            inbuffer.erase(0, total_len);

            std::cout << "处理后#inbuffer:\n " << inbuffer << std::endl;

            break;
        }
        else
            return false;
    }
    return true;
}

该代码中的知识点:

  • enLength:给文本加上长度前缀和分隔符,格式为 "content_len"\r\n"text"\r\n。
  • deLength:从报文中提取正文,去掉长度前缀和分隔符。
  • Request 类封装了操作数x、y和运算符op,提供 serialize 和 deserialize 方法。
  • Response 类封装了退出码和计算结果,同样提供序列化和反序列化。
  • recvPackage 函数从socket接收数据,处理粘包问题,保证读到一个完整的报文。
  • 使用 #ifdef MYSELF 条件编译,可以选择自己写的序列化方式或使用jsoncpp。

服务端:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include "log.hpp"
#include "Protocol.hpp"

namespace server
{
    enum
    {
        USAGE_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        LISTEN_ERR
    };

    static const uint16_t gport = 8080;
    static const int gbacklog = 5;

    typedef std::function<bool(const Request &req, Response &resp)> func_t;

    void handlerEntery(int sock, func_t func)
    {
        std::string inbuffer;
        while (true)
        {
            std::string req_text, req_str;
            if (!recvPackage(sock, inbuffer, &req_text))
                return;
            std::cout << "带报头的请求:\n" << req_text << std::endl;
            if (!deLength(req_text, &req_str))
                return;
            std::cout << "去掉报头的正文:\n" << req_str << std::endl;

            Request req;
            if (!req.deserialize(req_str))
                return;

            Response resp;
            func(req, resp);

            std::string resp_str;
            resp.serialize(&resp_str);

            std::cout << "计算完成, 序列化响应: " <<  resp_str << std::endl;

            std::string send_string = enLength(resp_str);
            std::cout << "构建完成完整的响应\n" <<  send_string << std::endl;
        
            send(sock, send_string.c_str(), send_string.size(), 0);
        }
    }

    class CalServer
    {
    public:
        CalServer(const uint16_t &port = gport) : _listensock(-1), _port(port)
        {
        }
        void initServer()
        {
            _listensock = socket(AF_INET, SOCK_STREAM, 0);
            if (_listensock < 0)
            {
                logMessage(FATAL, "create socket error");
                exit(SOCKET_ERR);
            }
            logMessage(NORMAL, "create socket success: %d", _listensock);

            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(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                logMessage(FATAL, "bind socket error");
                exit(BIND_ERR);
            }
            logMessage(NORMAL, "bind socket success");

            if (listen(_listensock, gbacklog) < 0)
            {
                logMessage(FATAL, "listen socket error");
                exit(LISTEN_ERR);
            }
            logMessage(NORMAL, "listen socket success");
        }
        void start(func_t func)
        {
            for (;;)
            {
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
                if (sock < 0)
                {
                    logMessage(ERROR, "accept error, next");
                    continue;
                }
                logMessage(NORMAL, "accept a new link success, get new sock: %d", sock);

                pid_t id = fork();
                if (id == 0)
                {
                    close(_listensock);
                    handlerEntery(sock, func);
                    close(sock);
                    exit(0);
                }
                close(sock);

                pid_t ret = waitpid(id, nullptr, 0);
                if (ret > 0)
                {
                    logMessage(NORMAL, "wait child success");
                }
            }
        }

        ~CalServer() {}

    private:
        int _listensock;
        uint16_t _port;
    };

}

该代码中的知识点:

  • handlerEntery 是处理入口,负责接收请求、反序列化、调用业务函数、序列化响应、发送响应。
  • recvPackage 保证读到一个完整的报文,处理粘包问题。
  • deLength 去掉长度前缀,得到正文。
  • req.deserialize 反序列化得到结构化的Request对象。
  • func(req, resp) 调用业务函数,填充Response。
  • resp.serialize 序列化响应。
  • enLength 加上长度前缀,构建完整报文。
  • send 发送响应。

客户端:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "Protocol.hpp"

#define NUM 1024

class CalClient
{
public:
    CalClient(const std::string &serverip, const uint16_t &serverport) 
    : _sock(-1)
    ,_serverip(serverip)
    , _serverport(serverport)
    {}
    void initClient()
    {
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if(_sock < 0)
        {
            std::cerr << "socket create error" << std::endl;
            exit(2);
        }
    }
    void start()
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(_serverport);
        server.sin_addr.s_addr = inet_addr(_serverip.c_str());

        if(connect(_sock, (struct sockaddr*)&server, sizeof(server)) != 0)
        {
            std::cerr << "socket connect error" << std::endl;
        }
        else
        {
            std::string line;
            std::string inbuffer;
            while(true)
            {
                std::cout << "mycal>>> ";
                std::getline(std::cin, line);
                Request req = ParseLine(line);

                std::string content;
                req.serialize(&content);
                std::string send_string = enLength(content);
                send(_sock,send_string.c_str(),send_string.size(),0);

                std::string package,text;
                if(!recvPackage(_sock,inbuffer,&package)) continue;
                if (!deLength(package, &text))
                    continue;

                Response resp;
                resp.deserialize(text);
                std::cout << "exitCode: " << resp.exitcode << std::endl;
                std::cout << "result: " << resp.result << std::endl;
            }
        }
    }

    Request ParseLine(const std::string &line)
    {
        int status = 0;
        int i = 0;
        int cnt = line.size();
        std::string left, right;
        char op;
        while (i < cnt)
        {
            switch (status)
            {
            case 0:
            {
                if(!isdigit(line[i]))
                {
                    op = line[i];
                    status = 1;
                }
                else left.push_back(line[i++]);
            }
            break;
            case 1:
                i++;
                status = 2;
                break;
            case 2:
                right.push_back(line[i++]);
                break;
            }
        }
        std::cout << std::stoi(left)<<" " << std::stoi(right) << " " << op << std::endl;
        return Request(std::stoi(left), std::stoi(right), op);
    }

    ~CalClient()
    {
        if(_sock >= 0) close(_sock);
    }
private:
    int _sock;
    std::string _serverip;
    uint16_t _serverport;
};

该代码中的知识点:

  • ParseLine 是一个状态机,解析用户输入的 "1+1" 格式,提取操作数x、y和运算符op。
  • req.serialize 序列化请求。
  • enLength 加上长度前缀。
  • send 发送请求。
  • recvPackage 接收响应,处理粘包问题。
  • deLength 去掉长度前缀。
  • resp.deserialize 反序列化响应。
  • 打印退出码和计算结果。

日志模块:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>

#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

const char * to_levelstr(int level)
{
    switch(level)
    {
        case DEBUG : return "DEBUG";
        case NORMAL: return "NORMAL";
        case WARNING: return "WARNING";
        case ERROR: return "ERROR";
        case FATAL: return "FATAL";
        default : return nullptr;
    }
}

void logMessage(int level, const char *format, ...)
{
#define NUM 1024
    char logprefix[NUM];
    snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid: %d]",
        to_levelstr(level), (long int)time(nullptr), getpid());

    char logcontent[NUM];
    va_list arg;
    va_start(arg, format);
    vsnprintf(logcontent, sizeof(logcontent), format, arg);

    std::cout << logprefix << logcontent << std::endl;
}

该代码中的知识点:

  • to_levelstr:将日志等级转为字符串。
  • logMessage:可变参数函数,使用 va_list、va_start、vsnprintf 处理可变参数。
  • snprintf:安全格式化字符串,防止缓冲区溢出。
  • time(nullptr):获取Unix时间戳。
  • getpid():获取当前进程PID。

相关推荐
@Mike@44 分钟前
13-数据库学习笔记(查询执行处理模型)
数据库·笔记·学习
Go Work1 小时前
实体类字段用 @Value 赋了默认值,取出来还是 null?
经验分享·笔记
水饺编程1 小时前
第1章:下载 Linux 0.12 内核
linux·c语言·汇编·ubuntu
白杨尚青1 小时前
C++入门篇(十三):vector(上)——动态数组:构造、空间增长与迭代器失效
开发语言·c++·笔记·算法·stl
牢姐与蒯1 小时前
一点小碎片——每个线程独有的东西——上下文+栈
linux·运维·服务器
小此方1 小时前
Linux网络(二十):TCP拥塞控制与延迟应答详解:从拥塞窗口到TCP性能优化,理解TCP与UDP的效率差异
linux·网络·php
yunwei371 小时前
eBPF 开发实践:使用 eBPF 隐藏进程或文件信息
linux·后端·性能优化
芝麻开门2921 小时前
Linux文件系统
linux·文件系统
羔羊++1 小时前
24_实验二十三_运行内核
linux