Linux Socket 编程全解析:UDP 与 TCP 实现及应用
Linux-Socket编程
1. Socket编程基础
1.1 理解源IP地址和目的IP地址
-
IP在网络中,用来表示主机的唯一性。 -
但是光有
IP是不够的,操作系统中可能运行很多的进程,那么操作系统怎么知道要把这个数据传输给哪个进程? -
因此,数据传输到主机是手段,而数据交给主机内的某一个进程才是目的。
1.2 理解端口号
端⼝号( port )是传输层协议的内容。
-
端口号是一个
2字节16位的整数。 -
端口号用来标识一个进程,告诉操作系统,当前这个数据要交给哪一个进程来处理。
-
IP地址 + 端口号能够标识网络上某一台主机的某一个进程。 -
一个端口号只能被一个进程占用,但一个进程可以绑定多个端口号。
-
0 - 1023: 知名端⼝号,这些端口号被分配给最重要、最广泛使用的网络服务,如HTTP、FTP、SSH等这些⼴为使⽤的应⽤层协议,通常由系统进程或特权程序使用。 -
1024 - 65535: 操作系统动态分配的端口号。客户端程序的端口号,就是由操作系统从这个范围分配的。
端口号和进程ID的区别:
- 进程ID是系统当中的概念,而端口号是网络中的概念。尽管进程PID在系统内具有唯一性,但若将其直接用于网络标识,会导致进程管理与网络通信这两个核心层产生强耦合。为避免这种设计缺陷,实践中采用了独立的端口号机制。
理解源端口和目的端口:
- 传输层协议( TCP 和 UDP )的数据段中有两个端⼝号,分别叫做源端⼝号和⽬的端⼝号。就是在描述数据是谁发的,要发给谁。
1.3 理解socket
-
IP地址用来标识互联网中唯一的一台主机,port用来标识该主机上唯一的一个网络进程。即IP + Port就能标识互联网中唯一的一个进程。 -
{src_ip , src_port , dest_ip , dest_port}标识互联网中唯二的两个进程。 -
ip + port被称为socket。
1.4 网络字节序
1.4.1 大端和小端
大端序和小端序指的就是数据在内存中存储字节的顺序。
-
大端: 高权值位存储到低地址处。
-
小端: 低权值位存储到低地址处。
假设我们要在内存中存储一个32位的十六进制数:0x12345678。
这个数由4个字节组成:
-
0x12是 最高有效字节 -
0x34 -
0x56 -
0x78是 最低有效字节
// 低地址 -> 高地址
// 大端:0x12 0x34 0x56 0x78
// 小端:0x78 0x56 0x34 0x12
1.4.2 网络中收发数据注意事项
-
发送主机通常将发送缓冲区中的数据按 内存地址从低到⾼的顺序发出。
-
接收主机把从⽹络上接到的字节依次保存在接收缓冲区中 按内存地址从低到⾼的顺序保存。
-
因此,先发出的数据是低地址,后发出的数据是⾼地址。
-
TCP/IP协议规定,网络数据流应采用大端字节序。
-
如果当前发送主机是小端,就要先将数据转换为大端。
1.4.3 htonl/htons/ntohl/ntohs
通过库函数实现网络字节序和主机字节序的转换
#include <arpa/inet.h>
// h: host n: network l: 32位整数 s: 16位整数
uint32_t htonl(uint32_t hostlong); // 将32位的整数从主机字节序转换为网络字节序
uint16_t htons(uint16_t hostshort); // 将16位的整数从主机字节序转换为网络字节序
uint32_t ntohl(uint32_t netlong); // 将32位的整数从网络字节序转换为主机字节序
uint16_t ntohs(uint16_t netshort); // 将16位的整数从网络字节序转换为主机字节序
若主机是大端字节序,则函数什么也不做。
2. Udp Socket
socket会有很多种类,来满足不同的应用场景,而socket的设计者成功地用一种通信接口抽象了本地和网络的通信场景。
udp的特点:
-
传输层协议。
-
无连接。
-
不可靠传输。
-
面向数据报。
-
全双工。
2.1 socket
创建 socket 文件描述符(TCP/UDP,客户端 + 服务器)。
#include <sys/types.h>
#include <sys/socket.h>int socket(int domain, int type, int protocol);
参数:
-
domain: 协议家族。-
AF_INET- IPv4协议 -
AF_INET6- IPv6协议 -
AF_UNIX- 本地套接字(进程间通信)
-
-
type: 套接字类型。-
SOCK_STREAM- 面向连接的字节流(如 TCP) -
SOCK_DGRAN- 无连接的数据报(如 UDP) -
SOCK_RAW- 原始套接字
-
-
protocol: 通常设置为0,表示根据前两个参数自动选择默认协议。
返回值:
-
成功:返回一个非负整数,表示新创建套接字的文件描述符。
-
失败:返回 -1,并设置全局变量
errno以指示具体错误。
2.2 bind
绑定IP、端口号等信息(TCP/UDP,服务器)。
#include <sys/types.h>
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
-
sockfd: 套接字描述符,即socket()函数成功返回的文件描述符。 -
addr: 指向结构体的指针,该结构体包含了要绑定的地址(IP地址和端口号,或文件路径)。这是一个通用指针,实际传入的可能是struct sockaddr_in(网络通信)、或struct sockaddr_un(本地通信)等。 -
addrlen: 结构体的实际大小,通常是sizeof(struct sockaddr_in)
返回值:
-
成功:返回 0。
-
失败:返回 -1,并设置全局变量
errno以指示具体错误。
服务端必须要通过 bind() 显示的指定监听端口。
客户端需要绑定,但是无需显示的bind(),首次发送消息时,OS会自动给client进行bind(),操作系统知道IP,端口号采取随机端口号的方式,因为一个端口号只能被一个进程所绑定,为了避免client端口冲突。而client端口是几不重要,只要是唯一即可。
2.3 struct sockaddr结构
2.3.1 结构介绍
struct sockaddr 相关结构体定义在 #include <netinet/in.h> 等头文件中。
Socket API 是一层抽象的网络编程接口,适用于各种底层网络协议,如 IPv4、IPv6,以及本地的进程间通信。然而,各种网络协议和本地通信的地址格式并不相同。因此,设计者采用了一种 C 语言风格的多态方式,设计了不同的地址结构体:
-
struct sockaddr作为通用的地址基类,在结构体头部包含一个地址族标识sa_family和一个通用数据字段sa_data。 -
struct sockaddr_in用于 IPv4 通信,且在结构体头部包含一个地址族标识sa_family,而其余字段明确包含了 IPv4 地址和端口号等字段。 -
struct sockaddr_un用于本地进程间通信,同样的在结构体头部包含了sa_family,其余字段是一个文件系统路径。
在使用时,通过将 sockaddr_in 或 sockaddr_un 等具体结构体的指针,强制转换为 sockaddr* 类型,传递给 bind、connect 等套接字函数。函数内部则根据结构体头部的 sa_family 字段判断实际的地址类型,从而执行相应的操作。这种设计以统一的接口屏蔽了底层不同协议的差异,实现了灵活而简洁的多态机制。
该设计让网络函数能够以一致的方式接收和处理各种不同类型结构体的,具体是什么类型的地址,由其中的 sa_family 字段在运行时动态决定。
-
IPv4地址⽤sockaddr_in结构体表⽰,包括16位地址类型、 16位端⼝号和32位IP地址。
-
struct sockaddr中的 14字节地址数据有什么用?-
因为历史的原因,最初的设计意图是让
struct sockaddr作为一个通用结构,其头部的sa_family字段作为类型标签,尾部的sa_data[14]字节数组作为数据载荷;同时期望像struct sockaddr_in这样的具体结构,能将其所有成员除了头部的地址类型(如端口号、IP地址等)序列化后完整地放入这14字节中。但是后期的结构体(如 struct sockaddr_un)超出了,所以这14字节也不在使用。 -
当前的方案是取出头部的地址类型,来判断是哪种类型的结构体,通过结构体指针强转访问对应的成员。
-

2.3.2 结构体原型
struct sockaddr结构体原型(统一的基类)
typedef unsigned short int sa_family_t;
// ##: 将两边的符号合并为一个符号
#define __SOCKADDR_COMMON(sa_prefix) \sa_family_t sa_prefix##familystruct sockaddr
{// 宏替换后: sa_family_t sa_family__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */char sa_data[14]; /* Address data. */
};
struct sockaddr_in结构体原型(具体的派生类)
typedef uint16_t in_port_t; // 等价 unsigned short inttypedef uint32_t in_addr_t; // 等价 unsigned int
struct in_addr
{in_addr_t s_addr;
};struct sockaddr_in
{ // 和 struct sockaddr 类似 宏替换__SOCKADDR_COMMON (sin_);in_port_t sin_port; /* Port number. */struct in_addr sin_addr; /* Internet address. */// 以下为填充字段/* Pad to size of `struct sockaddr'. */unsigned char sin_zero[sizeof (struct sockaddr)- __SOCKADDR_COMMON_SIZE- sizeof (in_port_t)- sizeof (struct in_addr)];
};
struct sockaddr 体现出一种巧妙的C语言多态设计!
2.3.3 struct sockaddr_in的填充
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <string>std::string server_ip = ...;
u_int16_t server_port = ...;struct sockaddr_in local;
bzero(&local , sizeof(local)); // 将 local 所有字节初始化
local.sin_family = AF_INET;
// 端口和IP需要发送到网络中,需要本地字节序转网络字节序
local.sin_port = htons(_port);)
// inet_adddr函数的功能:
// - 1.IP需要将点分十进制的字符串转换为4字节整数
// - 2.再将4字节整数转换为网络序列(大端字节序dr函数将以上2步全部做了
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 但通常服务器不需要绑定具体的IP地址,而使用任意地址绑定
local.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0 本质是整数0 所以无论小端还是大端都是0
为什么使用 INADDR_ANY 而不是绑定具体的IP地址
-
INADDR_ANY是0.0.0.0代表所有地址。 -
因为有公网、内网和本地的
127.0.0.1本地环回等多种访问来源。如果服务器在绑定时指定了一个具体的IP(如内网IP),那么它的服务就只会在这个特定的内网IP地址上监听,这样一来:-
公网用户将无法通过服务器的公网IP访问到该服务。
-
本地用户也无法通过
127.0.0.1进行本地测试或访问。 -
甚至服务器本身的其他内网IP也无法用于连接。
-
-
这相当于把服务限制在了一个非常狭小的IP地址上,极大地限制了服务的灵活性。而使用
INADDR_ANY(即0.0.0.0)进行绑定,则意味着告诉操作系统:我不关心数据包是发送到哪个IP地址的,只要是这台主机,目标是这个端口,我全部接受。这样就实现了对服务器所有IP地址(包括公网、内网、本地回环)的统一监听,确保了服务可以通过任何可能的网络路径被访问到,无论是来自外部的公网请求、内部网络的调用,还是本机的自我测试,都能畅通无阻。
127.0.0.1 是一个特殊的IP地址,称为本地环回地址。当你在计算机上访问
127.0.0.1时,你并不是要连接到互联网或局域网中的其他设备,而是在与你自己的这台机器进行通信。网络数据包根本不会离开你的主机,而是在操作系统内部的网络协议栈中直接绕了回去。通常使用在测试与调试代码上。
2.3.4 inet_addr/inet_ntoa
点分十进制字符串的本地序列IP与4字节整数网络序列IP相互转换的函数。
#include <arpa/inet.h>// typedef uint32_t in_addr_t; // 等价 unsigned int
// 该函数主要完成:
// 1. 将点分十进制的字符串风格的ip地址转换为4字节整数
// 2. 再将4字节的本地字节序整数转换为网络序列的4字节整数
in_addr_t inet_addr(const char *cp);// inet_addr的逆过程
// 1. 4字节的网络序列ip转换为本地字节序列整数
// 2. 再将其转换为字符串风格的点分十进制
char *inet_ntoa(struct in_addr in); // sockaddr_in中的struct in_addr sin_addr字段;
2.3.5 inet_pton/inet_ntop(推荐)
和 2.3.4 功能是一样的,但该函数功能更丰富。并且inet_ntoa 存在问题。点分十进制字符串的本地序列IP与4字节整数网络序列IP相互转换的函数。
-
p可以理解为进程(进程(本地序列) -> 网络序列)。
-
n可以理解为网络(网络序列 -> 进程(本地序列))。
-
其中
inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接⼝是void *addrptr。 -
关于inet_ntoa
- inet_ntoa 这个函数返回一个 char* ,该函数内部其实是使用类似
static buffer[64]的静态数据返回的,这导致如果我们多次调用该函数,本次调用会覆盖上一次调用的buffer,所以该函数不是线程安全的函数。
- inet_ntoa 这个函数返回一个 char* ,该函数内部其实是使用类似
inet_pton 函数用于将点分十进制的IP地址字符串转换为网络字节序的二进制格式。
#include <arpa/inet.h>int inet_pton(int af, const char *src, void *dst);
参数:
-
af: 协议家族-
AF_INET- IPv4地址 -
AF_INET6- IPv6地址
-
-
src: 源字符串,指向要转换的IP地址字符串。 -
dst: 目标缓冲区,指向存储转换后二进制结果的缓冲区。
inet_ntop 函数是 inet_pton() 的逆操作,用于将网络字节序的二进制IP地址转换为可读的字符串格式。
#include <arpa/inet.h>const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数:
-
af: 协议家族-
AF_INET- IPv4地址。 -
AF_INET6- IPv6地址。
-
-
src: 指向要转换的IP地址。 -
dst: 目标缓冲区,指向存储转换后二进制结果的缓冲区。 -
size: 目标缓冲区的大小。
2.4 recvfrom
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: 接收数据的缓冲区指针。 -
len: 缓冲区大小。 -
flags: 通常为0,代表阻塞等待。 -
src_addr: 发送方的结构体指针。 -
addlen: 输出型参数,结构体的实际长度。
返回值:
- 成功: 返回接收到的字节数。
- 失败: 返回-1,并设置errno。
2.5 sendto
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,代表不使用任何特殊标志。 -
dest_addr: 接收方的结构体指针。 -
addlen: 输入型参数,结构体的大小。
返回值:
- 成功: 返回实际发送的字节数。
- 失败: 返回-1,并设置errno。
2.6 Udp Server(v1)
v1: 简单回显的服务器。
2.6.1 UdpServer.hpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <functional>
#include "Log.hpp"using method_t = std::function<std::string(const std::string&)>;using namespace LogModule;int default_sockfd = -1;class UdpServer{public:UdpServer(u_int16_t port , method_t handler) :_sockfd(default_sockfd),_port(port),_is_running(false),_handler_data(handler){}void init() {_sockfd = socket(AF_INET , SOCK_DGRAM , 0);if(_sockfd < 0) {LOG(LogLevel::FATAL) << "socket create error";exit(errno);}LOG(LogLevel::INFO) << "socket create success - sockfd: " << _sockfd;struct sockaddr_in local;bzero(&local , sizeof(local));local.sin_family = AF_INET;// 而端口和IP需要发送到网络local.sin_port = htons(_port);// 1.IP需要将点分十进制的字符串转换为4字节整数// 2.再将4字节整数转换为网络序列(大端字节序)// inet_addr函数将以上2步全部做了// local.sin_addr.s_addr = inet_addr(_ip.c_str());// 服务端不绑定特定的IP,使用 0.0.0.0 任意IP绑定local.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0int n = bind(_sockfd , (sockaddr*)&local , sizeof(local));if(n < 0) {LOG(LogLevel::FATAL) << "bind error";exit(errno);}LOG(LogLevel::INFO) << "bind success - sockfd: " << _sockfd;}void start() {_is_running = true;while(_is_running) {char buffer[1024];struct sockaddr_in client;socklen_t client_addrlen = sizeof(client);ssize_t n = recvfrom(_sockfd , buffer , sizeof(buffer) - 1 , 0 , (sockaddr*)&client , &client_addrlen);// n实际接收的字节数if(n > 0) {// 解析客户端的 sockaddr_in,由于是网络序列需要转换为本地字节序列u_int16_t client_port = ntohs(client.sin_port);std::string client_ip = inet_ntoa(client.sin_addr); // 4字节的网络序列ip -> 字符串风格的本地序列ipbuffer[n] = 0;// LOG(LogLevel::DEBUG) << "client_ip: " << client_ip << " "// << "client_port: " << client_port << " "// << "server recvfrom buffer: " << buffer;// "server echo: " + buffer 报错: const char* + char* 指针 + 指针非法的// std::string echo = "server echo: ";// echo += buffer;// 使用回调函数处理数据std::string result = _handler_data(buffer);sendto(_sockfd , result.c_str() , result.size() , 0 , (sockaddr*)&client , client_addrlen);}}}private:int _sockfd;// std::string _ip;u_int16_t _port;bool _is_running;method_t _handler_data;
};
2.6.2 UdpServer.cc
#include "UdpServer.hpp"
#include <memory>// 上层处理数据
std::string handler(const std::string& str) {std::string res = "server echo: ";res += str;return res;
}#define USAGE_EXIT 1// ip地址使用 0.0.0.0 无需绑定具体ip
// ./udpserver port
int main(int argc , char* argv[]) {if(argc != 2) {std::cout << "Usage: " << argv[0] << " port" << std::endl;exit(USAGE_EXIT);}u_int16_t port = std::stoi(argv[1]);ENABLE_CONSOLE_FLUSH_STRATEGY();std::unique_ptr<UdpServer> udp_server = std::make_unique<UdpServer>(port , handler);udp_server->init();udp_server->start();return 0;
}
2.6.3 UdpClient.cc
#include <iostream>
#include <cstdlib>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>#define USAGE_EXIT 1// ./udpclient server_ip server_port
int main(int argc , char* argv[]) {if(argc != 3) {std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;exit(USAGE_EXIT);}std::string server_ip = argv[1];u_int16_t server_port = std::stoi(argv[2]);// 1.创建套接字int sockfd = socket(AF_INET , SOCK_DGRAM , 0);if(sockfd < 0) {std::cerr << "socket create error" << std::endl;exit(errno);}// client不需要显示的bind()// 填写服务器信息struct sockaddr_in dest_addr;memset(&dest_addr , 0 , sizeof(dest_addr));dest_addr.sin_family = AF_INET;dest_addr.sin_port = htons(server_port);dest_addr.sin_addr.s_addr = inet_addr(server_ip.c_str());while(true) {std::cout << "client input: ";std::string in;std::getline(std::cin , in);// 2.发送消息int n = sendto(sockfd , in.c_str() , in.size() , 0 , (sockaddr*)&dest_addr , sizeof(dest_addr));(void)n;// 占位,这里的server和上面的dest_addr实际是相同的struct sockaddr_in server;socklen_t len;char buffer[1024];int m = recvfrom(sockfd , buffer , sizeof(buffer) - 1 , 0 , (sockaddr*)&server , &len);if(m > 0) {buffer[m] = 0;std::cout << buffer << std::endl;}}return 0;
}
2.6.4 Makefile
.PHONY:all
all: udpserver udpclientudpserver:UdpServer.ccg++ -o $@ $^ -std=c++17
udpclient:UdpClient.ccg++ -o $@ $^ -std=c++17.PHONY:clean
clean:rm -f udpclient udpserver
UdpServer中使用的LOG(),可替换为cout。
2.7 Udp Server(v2)
v2: 英文翻译中文的服务器。
2.7.1 UdpServer.hpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <functional>
#include "Log.hpp"
#include "InetAddr.hpp"using method_t = std::function<std::string(const std::string& , const InetAddr&)>;using namespace LogModule;int default_sockfd = -1;class UdpServer{public:UdpServer(u_int16_t port , method_t handler) :_sockfd(default_sockfd),_port(port),_is_running(false),_handler_data(handler){}void init() {_sockfd = socket(AF_INET , SOCK_DGRAM , 0);if(_sockfd < 0) {LOG(LogLevel::FATAL) << "socket create error";exit(errno);}LOG(LogLevel::INFO) << "socket create success - sockfd: " << _sockfd;struct sockaddr_in local;bzero(&local , sizeof(local));local.sin_family = AF_INET;// 而端口和IP需要发送到网络local.sin_port = htons(_port);// 1.IP需要将点分十进制的字符串转换为4字节整数// 2.再将4字节整数转换为网络序列(大端字节序)// inet_addr函数将以上2步全部做了// local.sin_addr.s_addr = inet_addr(_ip.c_str());// 服务端不绑定特定的IP,使用 0.0.0.0 任意IP绑定local.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0int n = bind(_sockfd , (sockaddr*)&local , sizeof(local));if(n < 0) {LOG(LogLevel::FATAL) << "bind error";exit(errno);}LOG(LogLevel::INFO) << "bind success - sockfd: " << _sockfd;}void start() {_is_running = true;while(_is_running) {char buffer[1024];struct sockaddr_in client;socklen_t client_addrlen = sizeof(client);ssize_t n = recvfrom(_sockfd , buffer , sizeof(buffer) - 1 , 0 , (sockaddr*)&client , &client_addrlen);// n实际接收的字节数if(n > 0) {// 解析客户端的 sockaddr_in,由于是网络序列需要转换为本地字节序列// u_int16_t client_port = ntohs(client.sin_port);// std::string client_ip = inet_ntoa(client.sin_addr); // 4字节的网络序列ip -> 字符串风格的本地序列ip// 输入的是一个英文单词buffer[n] = 0;// LOG(LogLevel::DEBUG) << "client_ip: " << client_ip << " "// << "client_port: " << client_port << " "// << "server recvfrom buffer: " << buffer;// "server echo: " + buffer 报错: const char* + char* 指针 + 指针非法的// std::string echo = "server echo: ";// echo += buffer;// 使用回调函数处理数据std::string result = _handler_data(buffer , InetAddr(client));sendto(_sockfd , result.c_str() , result.size() , 0 , (sockaddr*)&client , client_addrlen);}}}private:int _sockfd;// std::string _ip;u_int16_t _port;bool _is_running;method_t _handler_data;
};
2.7.2 UdpServer.cc
#include "UdpServer.hpp"
#include "Dictionary.hpp"
#include <memory>#define USAGE_EXIT 1// ip地址使用 0.0.0.0 无需绑定具体ip
// ./udpserver port
int main(int argc , char* argv[]) {if(argc != 2) {std::cout << "Usage: " << argv[0] << " port" << std::endl;exit(USAGE_EXIT);}u_int16_t port = std::stoi(argv[1]);ENABLE_CONSOLE_FLUSH_STRATEGY();// 1.字典类Dictionary dict;dict.loadDictionaryFile(); // 加载数据// 2.udp网络通信类,两个类通过回调函数交互std::unique_ptr<UdpServer> udp_server = std::make_unique<UdpServer>(port , [&dict](const std::string& english_word , const InetAddr& addr)->std::string{return dict.translate(english_word , addr);});udp_server->init();udp_server->start();return 0;
}
2.7.3 UdpClient.cc
#include <iostream>
#include <cstdlib>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>#define USAGE_EXIT 1// ./udpclient server_ip server_port
int main(int argc , char* argv[]) {if(argc != 3) {std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;exit(USAGE_EXIT);}std::string server_ip = argv[1];u_int16_t server_port = std::stoi(argv[2]);// 1.创建套接字int sockfd = socket(AF_INET , SOCK_DGRAM , 0);if(sockfd < 0) {std::cerr << "socket create error" << std::endl;exit(errno);}// client不需要显示的bind()// 填写服务器信息struct sockaddr_in dest_addr;memset(&dest_addr , 0 , sizeof(dest_addr));dest_addr.sin_family = AF_INET;dest_addr.sin_port = htons(server_port);dest_addr.sin_addr.s_addr = inet_addr(server_ip.c_str());while(true) {std::cout << "client input: ";std::string in;std::getline(std::cin , in);if(in.empty()) {continue;}// 2.发送消息int n = sendto(sockfd , in.c_str() , in.size() , 0 , (sockaddr*)&dest_addr , sizeof(dest_addr));(void)n;// 占位,这里的server和上面的dest_addr实际是相同的struct sockaddr_in server;socklen_t len;char buffer[1024];int m = recvfrom(sockfd , buffer , sizeof(buffer) - 1 , 0 , (sockaddr*)&server , &len);if(m > 0) {buffer[m] = 0;std::cout << buffer << std::endl;}}return 0;
}
2.7.4 Dictionary.hpp
#pragma once#include "Log.hpp"
#include <unordered_map>
#include <fstream>
#include "InetAddr.hpp"#define OPEN_FILE_ERRO 1std::string sep = ": ";
std::string default_file_pathname = "./Dictionary.txt";// 英文翻译成中文的类
class Dictionary{public:Dictionary(const std::string& pathname = default_file_pathname) :_file_pathanme(pathname){}bool loadDictionaryFile() {std::ifstream in(_file_pathanme);if(!in.is_open()) {LogModule::LOG(LogModule::LogLevel::WARNING) << "文件打开失败";return false;}std::string line;while(std::getline(in , line)) {// 1. 解析一行字符串内容 格式 apple: 苹果size_t pos = line.find(sep);if(pos == std::string::npos) {// 格式错误: 没有找到分隔符LogModule::LOG(LogModule::LogLevel::WARNING) << " [格式错误]";continue;}std::string english_word = line.substr(0 , pos);std::string chinese_word = line.substr(pos + sep.size());if(english_word.empty() || chinese_word.empty()) {LogModule::LOG(LogModule::LogLevel::WARNING) << line << " [格式错误]";continue;}// 2. 添加到哈希表中_dict_hash.insert({english_word , chinese_word});LogModule::LOG(LogModule::LogLevel::INFO) << line << " [加载成功]";}in.close();return true;}std::string translate(const std::string& english_word , const InetAddr& addr) {std::unordered_map<std::string , std::string>::iterator iter = _dict_hash.find(english_word);if(iter == _dict_hash.end()) {LogModule::LOG(LogModule::LogLevel::INFO) << "进入翻译模块: "<< "client ip: "<< addr.getIP() << " "<< "client port: " << addr.getPort() << " "<< english_word << " -> " << "None";return "None";}LogModule::LOG(LogModule::LogLevel::INFO) << "进入翻译模块: "<< "client ip: "<< addr.getIP() << " "<< "client port: " << addr.getPort() << " "<< english_word << " -> " << iter->second;return iter->second;}private:std::string _file_pathanme;std::unordered_map<std::string , std::string> _dict_hash;
};
2.7.5 InetAddr.hpp
#pragma once#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>// 解析IP地址和端口号的类
class InetAddr{public:// 这个构造函数用来将 struct sockaddr_in 结构体转换为 // - 1.本地序列的字符串风格的点分十进制的IP // - 2.本地序列的整数端口InetAddr(const struct sockaddr_in& addr) :_addr(addr){_ip = inet_ntoa(addr.sin_addr);_port = ntohs(addr.sin_port);}const std::string& getIP() const { return _ip; }u_int16_t getPort() const { return _port; }const struct sockaddr_in& getInetAddr() const { return _addr; } // 格式化显示IP + Portstd::string showIpPort() {return "[" + _ip + " : " + std::to_string(_port) + "]";}private:struct sockaddr_in _addr;std::string _ip;u_int16_t _port;
};
2.8 Udp Server(v3)
v3: 简易群聊功能。
2.8.1 UdpServer.hpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <functional>
#include "Log.hpp"
#include "InetAddr.hpp"using method_t = std::function<void(int , const std::string ,const InetAddr&)>;using namespace LogModule;int default_sockfd = -1;class UdpServer{public:UdpServer(u_int16_t port , method_t handler) :_sockfd(default_sockfd),_port(port),_is_running(false),_handler_data(handler){}void init() {_sockfd = socket(AF_INET , SOCK_DGRAM , 0);if(_sockfd < 0) {LOG(LogLevel::FATAL) << "socket create error";exit(errno);}LOG(LogLevel::INFO) << "socket create success - sockfd: " << _sockfd;struct sockaddr_in local;bzero(&local , sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0int n = bind(_sockfd , (sockaddr*)&local , sizeof(local));if(n < 0) {LOG(LogLevel::FATAL) << "bind error";exit(errno);}LOG(LogLevel::INFO) << "bind success - sockfd: " << _sockfd;}void start() {_is_running = true;while(_is_running) {char buffer[1024];struct sockaddr_in client;socklen_t client_addrlen = sizeof(client);ssize_t n = recvfrom(_sockfd , buffer , sizeof(buffer) - 1 , 0 , (sockaddr*)&client , &client_addrlen);if(n > 0) {// 接收到某个用户发送的消息buffer[n] = 0;// 将该消息交给消息转发类处理_handler_data(_sockfd , buffer ,InetAddr(client));//sendto(_sockfd , result.c_str() , result.size() , 0 , (sockaddr*)&client , client_addrlen);}}}private:int _sockfd;// std::string _ip;u_int16_t _port;bool _is_running;method_t _handler_data;
};
2.8.2 UdpServer.cc
#include "UdpServer.hpp"
#include "Transfer.hpp"
#include "ThreadPool.hpp"
#include <memory>#define USAGE_EXIT 1using thread_handler_t = std::function<void()>;// ip地址使用 0.0.0.0 无需绑定具体ip
// ./udpserver port
int main(int argc , char* argv[]) {if(argc != 2) {std::cout << "Usage: " << argv[0] << " port" << std::endl;exit(USAGE_EXIT);}u_int16_t port = std::stoi(argv[1]);ENABLE_CONSOLE_FLUSH_STRATEGY();// 1.创建消息转发的类Transfer transfer;// 2.服务器的线程池类ThreadPool<thread_handler_t>* tp = ThreadPool<thread_handler_t>::getInstance();// 2.udp网络通信类,将收到的消息交给线程池处理std::unique_ptr<UdpServer> udp_server = std::make_unique<UdpServer>(port , [&transfer , &tp](int transfd ,const std::string& message ,const InetAddr& addr) {thread_handler_t thread_handler = std::bind(&Transfer::forwardToOnlieUsers , &transfer , transfd , message , addr);tp->enqueue(thread_handler); // udp server 收到消息之后将数据和处理数据的函数打包为一个新函数(线程可以处理的函数类型)只需要入队列即可});udp_server->init();udp_server->start();// 线程回收tp->stop();tp->join(); return 0;
}
2.8.3 UdpClient.cc
#include <iostream>
#include <cstdlib>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <pthread.h>
#include <unistd.h>#define USAGE_EXIT 1std::string server_ip;
u_int16_t server_port = 0;/*
* 客户端需要两个线程:
* 1. 一个线程负责发消息
* 2. 一个线程负责收消息
* 如果只有一个线程,可能已经收到了消息,但是被发消息的cin阻塞,导致看不到消息
*/ void* sendHandler(void* args) {int sockfd = reinterpret_cast<long long>(args);// 填写服务器信息struct sockaddr_in dest_addr;memset(&dest_addr , 0 , sizeof(dest_addr));dest_addr.sin_family = AF_INET;dest_addr.sin_port = htons(server_port);dest_addr.sin_addr.s_addr = inet_addr(server_ip.c_str());while(true) {std::cout << "client input: "; // 向文件描述1标准输出打印,从而使发送消息和收消息打印消息分离(分离到不同的Shell中)std::string in;std::getline(std::cin , in);if(in.empty()) {continue;}// 2.发送消息int n = sendto(sockfd , in.c_str() , in.size() , 0 , (sockaddr*)&dest_addr , sizeof(dest_addr));(void)n;// debug: 退出消息是否能被其他用户获取到// sleep(1);// 3.用户退出if(in == "quit") {break; // 发送线程结束}}return nullptr;
}void* recvHandler(void* args) {int sockfd = reinterpret_cast<long long>(args);while(true) {// 占位,这里的 server 和sendHandler中的 dest_addr 实际是相同的struct sockaddr_in server;socklen_t len;char buffer[1024];int n = recvfrom(sockfd , buffer , sizeof(buffer) - 1 , 0 , (sockaddr*)&server , &len);if(n > 0) {buffer[n] = 0;std::cerr << buffer << std::endl; // 向文件描述2标准错误打印}}return nullptr;
}// ./udpclient server_ip server_port
int main(int argc , char* argv[]) {if(argc != 3) {std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;exit(USAGE_EXIT);}server_ip = argv[1];server_port = std::stoi(argv[2]);// 1.创建套接字int sockfd = socket(AF_INET , SOCK_DGRAM , 0);if(sockfd < 0) {std::cerr << "socket create error" << std::endl;exit(errno);}// client不需要显示的bind()// 0: sender 1: recverpthread_t tids[2];pthread_create(&tids[0] , nullptr , sendHandler , reinterpret_cast<void*>(sockfd));pthread_create(&tids[1] , nullptr , recvHandler , reinterpret_cast<void*>(sockfd));pthread_join(tids[0] , nullptr);// 发送消息线程结束// 取消收消息线程pthread_cancel(tids[1]);pthread_join(tids[1] , nullptr);std::cout << "send thread recv thread and main thread end" << std::endl;return 0;
}
2.8.4 Transfer.hpp
#pragma once#include <vector>
#include "Log.hpp"
#include "InetAddr.hpp"
#include "Mutex.hpp"using namespace LogModule;#define QUIT_MESSAGE "quit"// 这是一个给当前在线用户消息转发的类
class Transfer{bool is_first_online(const InetAddr& addr) {// 检测当前用户是否是刚上线用户for(size_t i = 0; i < _online_users.size(); i++) {if(_online_users[i] == addr) {// 已经存在return false;}}return true;}public:Transfer() = default;void forwardToOnlieUsers(int transfd , const std::string& message , const InetAddr& addr) {MutexGuard mg(_mutex); // 加锁保证互斥if(is_first_online(addr)) {_online_users.emplace_back(addr); // 添加新用户LOG(LogLevel::INFO) << addr.showIpPort() << " 用户上线啦!";}std::string transmit_message = addr.showIpPort() + "# " + message;// 向所有在线用户发送消息for(auto& user : _online_users) {sendto(transfd , transmit_message.c_str() , transmit_message.size() , 0 , (sockaddr*)&user.getInetAddr() , sizeof(user.getInetAddr()));}// 可能有用户需要下线if(message == QUIT_MESSAGE) {std::vector<InetAddr>::iterator iter = _online_users.begin();while(iter != _online_users.end()) {if(*iter == addr) {_online_users.erase(iter);break; // 删除后立即break,防止迭代器失效}}}}private:// 引入线程池之后,可能多个线程同时访问,需要加锁保证互斥(有新用户上线后,还没添加完,可能有线程就在转发消息,导致消息转发漏掉了)std::vector<InetAddr> _online_users; // 保存每一个上线用户,第一次发送消息代表上线Mutex _mutex;
};
2.8.5 Pthread.hpp
#pragma once#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <functional>
#include <cstdlib>
#include <utility>
#include "Mutex.hpp"// 封装pthread
class Thread{using func_t = std::function<void()>;// 因为成员函数具有隐含的this指针, 不满足 void*(*start_routine)(void*),需要声明为静态函数// 但是 _routine 函数需要使用类内成员, 所以需要通过 args 把 this 指针传进来static void* _routine(void* args) {Thread* self = static_cast<Thread*>(args);self->_is_running = true;self->_method();self->_is_running = false;return nullptr;}public:// 引用折叠,既可以传左值也可以传右值,而右值引用的属性是左值template<typename T>Thread(T&& method) :_tid(0)// 这里不能使用move的原因是不是如果外面传入一个左值,这里直接move掉,外面的对象可能就制空了,_method(std::forward<T>(method)) // 使用完美转发保持右值属性,这样右值就会调用移动构造,左值继续调用拷贝构造,_is_running(false),_is_detach(false){int current;{MutexGuard mg(_id_lock);current = id++;}_tname = "thread - " + std::to_string(current);}bool start(bool dt = false) {if(_is_running)return false; // 线程已经运行int n = pthread_create(&_tid , nullptr , _routine , (void*)this);if(n != 0) {std::cerr << "pthread_create error" <<std::endl;return false;}// 如果设置分离, 在启动后立即分离if(dt) {detach();} std::cout << "pthread_create success , thread name: " << _tname << std::endl;return true;}void join() {// 若分离则不需要joinif(_is_detach) {std::cout << "线程已经分离, 不能进行join" << std::endl;return;}int n = pthread_join(_tid , nullptr);if(n != 0) {std::cerr << "pthread_join error: " << n << std::endl;} else {std::cout << "pthread_join success , thread name: " << _tname << std::endl;_is_running = false;}}pthread_t gettid() { return _tid; }bool detach() { // 若分离 / 未启动则不需要detachif(_is_detach || !_is_running) {std::cout << "线程已经分离或线程未启动, 不能进行detach" << std::endl;return false;}int n = pthread_detach(_tid);if(n != 0) {std::cerr << "pthread_detach error" << std::endl;return false;} else {std::cout << "pthread_detach success , thread name: " << _tname << std::endl;_is_detach = true;return true;}}// 谨慎使用 cancel,可能造成资源泄漏bool cancel() {if(_is_running) {int n = pthread_cancel(_tid);if(n != 0) {std::cerr << "pthread_cancel error" << std::endl;return false;} else {std::cout << "pthread_cancel success" << std::endl;_is_running = false;return true;}}std::cout << "线程未启动" << std::endl;return false;}std::string& gettname() { return _tname; }~Thread() {// if (_is_running && !_is_detach) {// // 析构时如果线程还在运行且未分离,尝试join// pthread_join(_tid, nullptr);// }}private:pthread_t _tid; // 线程idstd::string _tname; // 线程名字func_t _method; // 线程执行的方法bool _is_running; // 线程是否在运行bool _is_detach; // 线程是否分离// void* _result; Mutex _id_lock;public:static int id;
};int Thread::id = 1;
2.8.6 ThreadPool.hpp
#pragma once#include <queue>
#include <vector>
#include "Pthread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
#include "Log.hpp"using namespace LogModule;const unsigned int default_num = 5;template<typename T>
class ThreadPool{// 每个线程所执行的任务函数,从任务队列中取任务void handlerTask() {// 取任务的时候,线程池可能关闭了,但是线程需要把队列中的任务全部处理完,才能停止while(true) {T task;{MutexGuard mg(_task_mutex);while(_q.empty() && _is_running) { // while防止伪唤醒// 1.如果队列为空并且线程池在运行则等待// 2.使用到了短路,若队列不为空,无论线程池是否关闭都应当继续执行队列中的任务,则短路,则继续往下进行处理任务// 3.若队列为空且线程池不运行,则退出while循环,进入下面的if条件LOG(LogLevel::INFO) << "队列为空,线程进入阻塞";_thread_wait_num++;_task_cond.wait(_task_mutex);_thread_wait_num--;}// 如果线程被唤醒,发现线程池关闭且队列中没有任务则退出循环if(_q.empty() && !_is_running) {break;}task = _q.front();_q.pop();}task(); // 处理任务在临界区外}} ThreadPool(unsigned int num = default_num):_num(num),_is_running(false){for(size_t i = 0; i < _num; i++) {// 这里传 Lambda 是因为 Thread中的任务类型是 std::function<void()>,而 handlerTask 默认第一个参数是 this,所以多包装一层// 而 Lambda 变量可见性规则是默认什么都看不到,包括成员变量和成员函数,所以需要传递this指针// emplace_back 无需拷贝或移动,直接原地构造_threads.emplace_back([this]{handlerTask();});}LOG(LogLevel::INFO) << "线程池创建" << _num << "个线程成功";}ThreadPool(const ThreadPool<T>&) = delete;ThreadPool<T>& operator=(const ThreadPool<T>&) = delete;void start() {if(_is_running)return;_is_running = true;for(auto& thread : _threads) {thread.start();}LOG(LogLevel::INFO) << "线程池开始运行";}public:// 懒汉单例模式// 必须使用 static 因为没有对象只能使用类名调用static ThreadPool<T>* getInstance() {// 这里加锁是必须的,但是当 _instance 已经不为 nullptr 了,而每次进入该函数还需要申请锁降低效率,所以多加一层if// 这里只是获取,不会影响并行的问题if(_instance == nullptr) {MutexGuard mg(_ins_mutex); if(_instance == nullptr) {_instance = new ThreadPool<T>; // 这里调用构造函数 ThreadPool_instance->start();//首次创建调用start()}}return _instance;}void stop() {if(!_is_running)return;MutexGuard mg(_task_mutex);_is_running = false;// 可能有还有线程在等待,所以需要唤醒所有等待的线程// 如果没有线程等待,这行代码也不影响// 这里需要加锁// 线程A(工作线程):在 handlerTask() 中检查队列,发现为空// 线程A:准备进入等待状态,_thread_wait_num++ 但还未执行// 线程B(主线程):调用 stop(),检查 _thread_wait_num 为0,不广播// 线程A:执行 _thread_wait_num++ 然后进入等待// 结果:线程A永远等待,无法被唤醒!if(_thread_wait_num > 0)_task_cond.broadcast();}void join() {for(auto& thread : _threads) {LOG(LogLevel::INFO) << "回收 " << thread.gettname() << " 线程成功";thread.join();}}// 可能有多个执行流入任务void enqueue(const T& task) {// 双重检查// 1.快速拒绝,避免不必要的锁竞争// 2.确保状态一致性if(!_is_running) // 当线程池停止时,不允许继续向任务队列中放任务return;MutexGuard mg(_task_mutex);if(!_is_running) return;_q.push(task);// 若有线程等待,则唤醒对应的线程执行任务if(_thread_wait_num > 0) {LOG(LogLevel::INFO) << "唤醒一个线程处理任务";_task_cond.signal();}}~ThreadPool() {LOG(LogLevel::DEBUG) << "~ThreadPool()";// 单例模式不能在析构函数中 delete _instance,会造成无限递归// if(_instance)// delete _instance// 程序调用 delete _instance// 系统开始销毁 _instance 指向的对象// 系统自动调用这个对象的析构函数 ~ThreadPool()// 在析构函数中又遇到 delete _instance// 回到步骤1,形成无限递归// 外部管理者控制单例的生命周期}private:std::queue<T> _q; // 任务队列,每个任务需要提供 operator() 方法 / std::functionstd::vector<Thread> _threads; // 管理每一个线程unsigned int _num; // 线程池中线程的数量bool _is_running; // 线程池是否运行Mutex _task_mutex;Cond _task_cond;unsigned int _thread_wait_num = 0;// 因为在 getInstance 函数中使用,没有调用构造函数,所以普通成员还不存在,则只能使用 static 的锁// 静态成员变量不会在构造函数的初始化列表中调用默认构造// static 成员变量本质就是全局变量,在编译时就已经创建,只不过需要受类域限制static ThreadPool<T>* _instance;static Mutex _ins_mutex;
}; template<typename T>
ThreadPool<T>* ThreadPool<T>::_instance = nullptr;template<typename T>
Mutex ThreadPool<T>::_ins_mutex; // 在这里调用默认构造函数
2.8.7 InetAddr.hpp
#pragma once#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <cstring>const std::string any_ip = "0.0.0.0";// 解析IP地址和端口号的类
class InetAddr{public:// 这个构造函数用来将 struct sockaddr_in 结构体转换为 // - 1.本地序列的字符串风格的点分十进制的IP // - 2.本地序列的整数端口// 网络转主机InetAddr(const struct sockaddr_in& addr) :_addr(addr){_port = ntohs(addr.sin_port);char ip_buffer[64];inet_ntop(AF_INET , &addr.sin_addr , ip_buffer, sizeof(ip_buffer));_ip = ip_buffer;}// 主机转网络// #define INADDR_ANY 0InetAddr(const std::string ip , u_int16_t port) :_ip(ip),_port(port){memset(&_addr , 0 , sizeof(_addr));_addr.sin_family = AF_INET;_addr.sin_port = htons(_port);inet_pton(AF_INET , _ip.c_str() , &_addr.sin_addr);}InetAddr(u_int16_t port) :_port(port),_ip(any_ip){memset(&_addr , 0 , sizeof(_addr));_addr.sin_family = AF_INET;_addr.sin_port = htons(_port);_addr.sin_addr.s_addr = INADDR_ANY; // inet_pton(AF_INET , _ip.c_str() , &_addr.sin_addr);}const std::string& getIP() const { return _ip; }u_int16_t getPort() const { return _port; }const struct sockaddr_in& getInetAddr() const { return _addr; } const struct sockaddr* getSockaddr() const { return (const struct sockaddr*)&_addr; }socklen_t getSockaddrLen() const { return sizeof(_addr); }// 格式化显示IP + Portstd::string showIpPort() const {return "[" + _ip + " : " + std::to_string(_port) + "]";}bool operator==(const InetAddr& addr) const {return _ip == addr.getIP() && _port == addr.getPort(); }private:struct sockaddr_in _addr;std::string _ip;u_int16_t _port;
};
3. Tcp Socket
3.1 socket(同Udp)
3.2 bind(同Udp)
3.3 listen
listen() 函数用于将套接字设置为被动监听模式,等待客户端的连接请求。
#include <sys/types.h>
#include <sys/socket.h>int listen(int sockfd, int backlog);
参数:
-
sockfd: 监听套接字,专门用于接收新连接,不直接通信,即socket()函数成功返回的文件描述符。 -
backlog: 等待连接队列的最大连接数。
返回值:
-
成功:返回 0。
-
失败:返回 -1,并设置 errno。
3.4 accept
accept() 函数用于接受客户端的连接请求,从已完成连接队列中取出一个连接。
#include <sys/types.h>
#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
-
sockfd: 通信套接字,与某一个客户端进行直接的通信(数据传输)。 -
addr: 指向struct sockaddr的指针,用于存储客户端地址信息。 -
addlen: 指向socklen_t的指针,输入时为 addr 缓冲区大小,输出时为实际地址长度。
返回值:
-
成功:返回新的套接字描述符,用于与客户端通信。
-
失败:返回 -1,并设置 errno。
监听套接字调用一次,通信套接字调用多次。
3.5 connect
connect() 函数用于客户端发起连接请求,连接到服务器。
#include <sys/types.h>
#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
参数:
-
sockfd: 客户端套接字描述符,由socket()创建。 -
addr: 指向服务器地址结构的指针。 -
addrlen: 服务器地址结构的长度。
返回值:
-
成功:返回 0。
-
失败:返回 -1,并设置 errno。
3.6 write/read
在 TCP socket 编程中,一旦连接建立成功,就可以使用标准的文件 I/O 函数 read() 和 write() 来进行数据传输。
#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
3.7 Tcp Server
以下代码中包含多进程版本、多线程版本和线程池版本,并远程执行命令的服务器。线程封装代码同Udp。
3.7.1 TcpServer.hpp
#pragma once#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
#include <signal.h>
#include <pthread.h>using task_t = std::function<void()>;
using callback_t = std::function<std::string(const std::string&)>; const int default_listenfd = -1;
const int default_backlog = 5;// 服务器禁止拷贝和赋值
class TcpServer : public NoCopy{public:TcpServer(u_int16_t port , callback_t callback) :_listenfd(default_listenfd),_port(port),_is_running(false),_callback(callback){}void init() {// signal(SIGCHLD , SIG_IGN);// 1.创建网络文件描述符_listenfd = socket(AF_INET , SOCK_STREAM , 0);if(_listenfd < 0) {LogModule::LOG(LogModule::LogLevel::FATAL) << "create socket error";exit(SOCKET_ERROR);}LogModule::LOG(LogModule::LogLevel::INFO) << "create socket success - listenfd: " << _listenfd;// 2.构造服务器信息并绑定InetAddr addr(_port);int n = bind(_listenfd , addr.getSockaddr() , addr.getSockaddrLen());if(n < 0) {LogModule::LOG(LogModule::LogLevel::FATAL) << "bind error";exit(BIND_ERROR);}LogModule::LOG(LogModule::LogLevel::INFO) << "bind success - listenfd: " << _listenfd;// 3.监听网络文件描述符n = listen(_listenfd , default_backlog);if(n < 0) {LogModule::LOG(LogModule::LogLevel::FATAL) << "listen error";exit(LISTEN_ERROR);}LogModule::LOG(LogModule::LogLevel::INFO) << "listen success - listenfd: " << _listenfd;}void server(int sockfd , const InetAddr& client) {while(true) {char buffer[1024];ssize_t n = read(sockfd , buffer , sizeof(buffer) - 1);if(n > 0) {// buffer是一个命令buffer[n] = 0;LogModule::LOG(LogModule::LogLevel::INFO) << "server read from " << client.showIpPort() << ": " << buffer;std::string result = "server result: \n";result += _callback(buffer); // 将数据进行回调处理write(sockfd , result.c_str() , result.size());} else if(n == 0) {// 客户端断开连接LogModule::LOG(LogModule::LogLevel::INFO) << "client disconnection";close(sockfd);break;} else {// 读取错误LogModule::LOG(LogModule::LogLevel::WARNING) << "read error";close(sockfd);break;}}}void start() {_is_running = true;while(_is_running) {struct sockaddr_in client;socklen_t addrlen = sizeof(client);// 4.与客户端建立连接并通信的文件描述符int sockfd = accept(_listenfd , (struct sockaddr*)&client , &addrlen);if(sockfd < 0) {continue; // 与客户端建立连接失败应继续与其他客户端建立}LogModule::LOG(LogModule::LogLevel::INFO) << "establish connection with client - sockfd: " << sockfd;InetAddr client_addr(client);// version1: 多进程 tcpserver/*pid_t n = fork();if(n == 0) { // child process// 关闭不需要的文件描述符close(_listenfd); // 监听是主进程做的事情,子进程只需要负责与一个客户端进行通信即可if(fork() > 0) // child process 直接退出 exit(OK);// child child process 孙子进程被系统领养并回收server(sockfd , client_addr);exit(OK);} else if(n < 0) {LogModule::LOG(LogModule::LogLevel::FATAL) << "fork error";exit(FORK_ERRO);}// parent process 不能阻塞的等,否则退化为单进程服务器// 1. 使用信号 signal(SIGCHLD , SIG_IGN);// 2. 子进程再创建子进程的方式执行server,之后子进程退出,孙子进程被1号系统进程领养并回收close(sockfd); // 这里会将子进程文件描述表中指向的struct file的引用计数-1*/// version2: 多线程 tcpserverpthread_t tid;ThreadDate* td = new ThreadDate(sockfd , client_addr , this);int n = pthread_create(&tid , nullptr , threadRoutine , td);// version3: 引入线程池// ThreadPool<task_t>::getInstance()->enqueue([this , sockfd , client_addr](){// this->server(sockfd , client_addr);// });}_is_running= false;}private:struct ThreadDate{ThreadDate(int fd , const InetAddr& addr , TcpServer* tcptr):sockfd(fd),client_addr(addr),tsver(tcptr){}int sockfd;InetAddr client_addr;TcpServer* tsver;};static void* threadRoutine(void* args) {pthread_detach(pthread_self());ThreadDate* td = static_cast<ThreadDate*>(args);td->tsver->server(td->sockfd , td->client_addr);delete td;return nullptr;}private:int _listenfd;// std::string _ip;u_int16_t _port;bool _is_running;callback_t _callback;
};
3.7.2 TcpServer.cc
#include "TcpServer.hpp"
#include "Command.hpp"void Usage(const std::string& out) {LogModule::LOG(LogModule::LogLevel::INFO) << "Usage: " << out << " port";exit(USAGE_ERROR);
}// tcpserver port
int main(int argc , char* argv[]) {if(argc != 2) {Usage(argv[0]);}u_int16_t port = std::stoi(argv[1]);Command command;std::unique_ptr<TcpServer> tser = std::make_unique<TcpServer>(port , std::bind(&Command::execute , &command , std::placeholders::_1));tser->init();tser->start();return 0;
}
3.7.3 TcpClient.cc
#include "Common.hpp"
#include "InetAddr.hpp"void Usage(const std::string& out) {std::cerr << "Usage: " << out << " server_ip serever_port" << std::endl;;exit(USAGE_ERROR);
}// tcpclient server_ip server_port
int main(int argc , char* argv[]) {if(argc != 3) {Usage(argv[0]);}std::string server_ip = argv[1];u_int16_t server_port = std::stoi(argv[2]);int sockfd = socket(AF_INET , SOCK_STREAM , 0);if(sockfd < 0) {std::cerr << "socket create error" << std::endl;exit(SOCKET_ERROR);}// 客户端需要bind,但是无需显示bind,操作系统会自动bind并采用随机端口的方式// 不需要bind,也不需要 listen 和 accpetInetAddr server_addr(server_ip , server_port);int n = connect(sockfd , server_addr.getSockaddr() , server_addr.getSockaddrLen());if(n < 0) {std::cerr << "connect error" << std::endl;exit(CONNECT_ERROR);}while(true) {std::string message;std::cout << "client input: ";std::getline(std::cin , message);int n = write(sockfd , message.c_str() , message.size());(void)n;char buffer[1024];n = read(sockfd , buffer , sizeof(buffer) - 1);if(n > 0) {buffer[n] = 0;std::cout << buffer << std::endl;}}close(sockfd);return 0;
}
3.7.4 Command.hpp
#pragma once
#include <iostream>
#include <sstream>
#include <set>
#include <string>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>std::vector<std::string> default_whitelist = {"ls" , "pwd" , "who" , "whoami" , "ll" , "ls -l"};class Command{public:Command() :_white_list(default_whitelist.begin() , default_whitelist.end()) {}std::string execute(const std::string& cmd) {// 判断是否在白名单中的命令std::set<std::string>::iterator iter = _white_list.find(cmd);if(iter == _white_list.end()) {// 不存在return "invalid command!\n"; }int pipefd[2]; // pipe[0]: read , pipe[1]: write// 1.创建管道int n = pipe(pipefd);if(n < 0) {std::cerr << "pipe create error" << std::endl;exit(1);}// 2.创建子进程pid_t pid = fork();if(pid < 0) {std::cerr << "fork error" << std::endl;exit(2);} else if(pid == 0) {// child process writeclose(pipefd[0]); // 3.将子进程的标准输出重定向到管道的写端dup2(pipefd[1] , 1);close(pipefd[1]);// 4.解析命令std::vector<std::string> tokens;std::stringstream ss(cmd == "ls" ? "ls -C" : cmd);std::string token;while(ss >> token) {tokens.emplace_back(token);}std::vector<char*> args;for(auto& t: tokens) {args.emplace_back(const_cast<char*>(t.c_str()));}args.push_back(nullptr);// 5.程序替换执行命令execvp(args[0] , args.data());exit(0);}// parent process readclose(pipefd[1]);char buffer[1024];ssize_t count = read(pipefd[0] , buffer , sizeof(buffer) - 1);if(count > 0) {buffer[count] = 0;} else if(count < 0) {waitpid(pid , nullptr , 0);std::cerr << "read error" << std::endl;exit(3);}// 6.回收子进程waitpid(pid , nullptr , 0);close(pipefd[0]);return buffer;}private:std::set<std::string> _white_list;
tr , 0);std::cerr << "read error" << std::endl;exit(3);}// 6.回收子进程waitpid(pid , nullptr , 0);close(pipefd[0]);return buffer;}private:std::set<std::string> _white_list;
};
