linux_服务端与客户端剖析
首先我们来聊聊套接字
套接字(Socket)是计算机网络中实现不同主机或同一主机内进程间通信的一种技术,是网络编程的基础。
简单来说,套接字就像两个进程之间的“连接端点”,通过它,进程可以发送和接收数据。它主要由IP地址和端口号组成,IP地址用于定位网络中的主机,端口号用于区分主机上的不同进程(比如浏览器用80端口,邮件服务用25端口等)。
套接字的类型
- 流式套接字(SOCK_STREAM):基于TCP协议,提供可靠、有序、双向的字节流传输,适合需要确保数据完整到达的场景(如网页浏览、文件传输)。
- 数据报套接字(SOCK_DGRAM):基于UDP协议,不保证数据传输的可靠性和顺序,但传输速度快,适合实时性要求高的场景(如视频通话、游戏)。
通过套接字,开发者可以编写网络程序,实现客户端与服务器之间的通信(比如客户端发送请求,服务器响应处理)。
log.hpp
#pragma once#include #include #include #include #include #include #include #include #define SIZE 1024#define Info 0#define Debug 1#define Warning 2#define Error 3#define Fatal 4#define Screen 1#define Onefile 2#define Classfile 3#define LogFile \"log.txt\"class Log{public: Log() { printMethod = Screen; path = \"./log/\"; } void Enable(int method) { printMethod = method; } std::string levelToString(int level) { switch (level) { case Info: return \"Info\"; case Debug: return \"Debug\"; case Warning: return \"Warning\"; case Error: return \"Error\"; case Fatal: return \"Fatal\"; default: return \"None\"; } } // void logmessage(int level, const char *format, ...) // { // time_t t = time(nullptr); // struct tm *ctime = localtime(&t); // char leftbuffer[SIZE]; // snprintf(leftbuffer, sizeof(leftbuffer), \"[%s][%d-%d-%d %d:%d:%d]\", levelToString(level).c_str(), // ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, // ctime->tm_hour, ctime->tm_min, ctime->tm_sec); // // va_list s; // // va_start(s, format); // char rightbuffer[SIZE]; // vsnprintf(rightbuffer, sizeof(rightbuffer), format, s); // // va_end(s); // // 格式:默认部分+自定义部分 // char logtxt[SIZE * 2]; // snprintf(logtxt, sizeof(logtxt), \"%s %s\\n\", leftbuffer, rightbuffer); // // printf(\"%s\", logtxt); // 暂时打印 // printLog(level, logtxt); // } void printLog(int level, const std::string &logtxt) { switch (printMethod) { case Screen: std::cout << logtxt << std::endl; break; case Onefile: printOneFile(LogFile, logtxt); break; case Classfile: printClassFile(level, logtxt); break; default: break; } } void printOneFile(const std::string &logname, const std::string &logtxt) { std::string _logname = path + logname; int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // \"log.txt\" if (fd tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec); va_list s; va_start(s, format); char rightbuffer[SIZE]; vsnprintf(rightbuffer, sizeof(rightbuffer), format, s); va_end(s); // 格式:默认部分+自定义部分 char logtxt[SIZE * 2]; snprintf(logtxt, sizeof(logtxt), \"%s %s\", leftbuffer, rightbuffer); // printf(\"%s\", logtxt); // 暂时打印 printLog(level, logtxt); }private: int printMethod; std::string path;};// int sum(int n, ...)// {// va_list s; // char*// va_start(s, n);// int sum = 0;// while(n)// {// sum += va_arg(s, int); // printf(\"hello %d, hello %s, hello %c, hello %d,\", 1, \"hello\", \'c\', 123);// n--;// }// va_end(s); //s = NULL// return sum;// }
main.cc
#include \"UdpServer.hpp\"#include #include // \"120.78.126.148\" 点分十进制字符串风格的IP地址void Usage(std::string proc){ std::cout << \"\\n\\rUsage: \" << proc << \" port[1024+]\\n\" << std::endl;}std::string Handler(const std::string &str){ std::string res = \"Server get a message: \"; res += str; std::cout << res < \"ls\" \"-a\" \"-l\" // // exec*(); // } return res;}// ./udpserver portint main(int argc, char *argv[]){ if(argc != 2) { Usage(argv[0]); exit(0); } uint16_t port = std::stoi(argv[1]); std::unique_ptr svr(new UdpServer(port)); svr->Init(/**/); svr->Run(Handler); return 0;}
Usage 函数
- 作用:提示用户程序的正确运行方式。
- 当命令行参数个数不正确时,输出提示信息(格式: 程序名 端口号 ,且端口号需大于 1024,因为 1-1023 是知名端口,通常被系统服务占用)。
Handler 函数
- 作用:定义消息处理逻辑,是服务器接收客户端消息后的具体处理方式。
- 逻辑:将客户端发送的字符串 str 拼接为 Server get a message: [客户端消息] 的格式,打印到服务器控制台,并将拼接后的字符串作为响应返回给客户端。
- 注释部分:预留了通过 fork() 创建子进程执行命令(如 ls -l )的扩展思路,可用于实现更复杂的处理逻辑(如执行系统命令并返回结果)。
主函数(main)核心逻辑
(1)参数解析与校验
- if(argc != 2) :检查命令行参数是否为 2 个(程序名 + 端口号),若不符合则调用 Usage 提示并退出。
- uint16_t port = std::stoi(argv[1]) :将命令行传入的端口号字符串转换为整数,作为服务器绑定的端口。
(2)创建 UdpServer 对象
- std::unique_ptr svr(new UdpServer(port)) :使用智能指针 unique_ptr 创建 UdpServer 实例,传入端口号 port 作为构造参数,避免手动管理内存(防止内存泄漏)。
(3)初始化与启动服务器
- svr->Init(/**/) :调用 UdpServer 类的 Init 方法初始化服务器(具体逻辑在 UdpServer.hpp 中实现,通常包括创建 UDP 套接字、绑定端口等)。
- svr->Run(Handler) :调用 UdpServer 类的 Run 方法启动服务器主循环,传入 Handler 函数作为消息处理回调。 Run 方法的核心逻辑通常是:循环接收客户端消息 → 调用 Handler 处理消息 → 将处理结果返回给客户端。
makefile
.PHONY:allall:udpserver udpclientudpserver:Main.ccg++ -o $@ $^ -std=c++11udpclient:UdpClient.ccg++ -o $@ $^ -std=c++11.PHONY:cleanclean:rm -f udpserver udpclient
UdpServer.hpp(服务端)

创建套接字(socket)
函数参数说明
1. domain (协议族):指定网络通信使用的地址类型,常用值:
- AF_INET :用于 IPv4 地址(最常用)。
- AF_INET6 :用于 IPv6 地址。
- AF_UNIX / AF_LOCAL :用于本地进程间通信(非网络)。
2. type (套接字类型):指定通信方式,常用值:
- SOCK_STREAM :流式套接字,基于 TCP 协议,提供可靠、有序、面向连接的通信(如 HTTP 服务)。
- SOCK_DGRAM :数据报套接字,基于 UDP 协议,提供不可靠、无连接的通信(如 DNS、实时视频)。
3. protocol (协议):指定具体协议,通常设为 0 ,表示根据前两个参数自动选择默认协议(如 SOCK_STREAM 对应 TCP, SOCK_DGRAM 对应 UDP)。
返回值
- 成功:返回一个非负整数(套接字描述符,类似文件描述符,用于后续操作)。
- 失败:返回 -1 ,并设置 errno 表示错误原因(如权限不足、协议不支持等)。
绑定套接字(bind)

首先设置地质结构struct socksddr_in local;
接着清空内存残留数据- bzero(&local, sizeof(local)) :将地址结构的所有字节清零,避免内存中残留的垃圾数据影响后续操作(比如未初始化的字段可能导致绑定失败)。
然后调用local设置ip协议(local.sin_family = AF_INET; // 使用IPv4协议),端口(local.sin_port = htons(port_); // 端口号转换为网络字节序(大端序)),
- 为什么需要 htons ?
计算机存储数据的字节序有两种:主机字节序(可能是小端序,如x86架构CPU)和网络字节序(固定为大端序)。网络通信中必须使用网络字节序,否则不同字节序的主机之间会解析出错。
htons (host to network short)的作用是将主机字节序的16位端口号转换为网络字节序(大端序)。
再设置local.sin_addr.s_addr = inet_addr(ip_.c_str()); // IP地址转换为网络字节序
- sin_addr.s_addr :存储服务器要绑定的IP地址(如\"192.168.1.100\")。
- inet_addr 的作用:
1. 将字符串形式的IP地址(如\"192.168.1.100\")转换为32位无符号整数(二进制形式)。
2. 自动将该整数转换为网络字节序(大端序),满足网络通信的要求。
- 如果服务器要绑定到所有本地网卡(比如服务器有多个网卡,希望通过任意网卡都能访问),可以用 INADDR_ANY 代替具体IP,写法为:
local.sin_addr.s_addr = htonl(INADDR_ANY);
( INADDR_ANY 是一个宏,值为0,代表“任意IP”; htonl 用于32位整数的字节序转换,与 htons 类似但针对4字节数据)。
最后绑定套接字
if(bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0){ lg(Fatal, \"bind error, errno: %d, err string: %s\", errno, strerror(errno)); exit(BIND_ERR);}
- bind 是系统调用,作用是将套接字( sockfd_ )与前面设置的地址结构( local )绑定,参数说明:
- 第一个参数 sockfd_ :要绑定的套接字描述符(之前 socket 函数创建的返回值)。
- 第二个参数 (const struct sockaddr *)&local :强制转换为通用地址结构 struct sockaddr* (因为 bind 函数设计为支持多种地址族,如IPv4、IPv6等,需要统一接口)。
- 第三个参数 sizeof(local) :地址结构的大小(告诉操作系统该结构的长度)。
- 返回值判断: bind 成功返回0,失败返回-1。如果失败,通过 errno 获取错误码(如端口被占用、IP地址不存在等),并通过 strerror(errno) 打印具体错误信息(如“Address already in use”表示端口已被占用),然后退出程序。n
需要注意,在云服务器上是禁止用户bind直接访问公网ip的
启动服务器处理请求
void Run(func_t func){ isrunning_ = true; char inbuffer[size]; while(isrunning_) { struct sockaddr_in client; socklen_t len = sizeof(client); // 接收客户端数据 ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len); if(n < 0) { lg(Warning, \"recvfrom error, errno: %d, err string: %s\", errno, strerror(errno)); continue; } inbuffer[n] = 0; // 手动添加字符串结束符 std::string info = inbuffer; // 调用回调函数处理数据 std::string echo_string = func(info); // 发送处理结果给客户端 sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (const sockaddr*)&client, len); }}
- recvfrom :从套接字接收客户端发送的数据,同时获取客户端的地址( client 结构)和地址长度( len )。
recvfrom 是网络编程中用于接收数据的函数,主要用于无连接的 UDP 协议,也可用于面向连接的 TCP 协议,其核心功能是从指定的套接字接收数据,并获取发送方的地址信息。
函数原型
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);主要参数说明
- sockfd :接收数据的套接字描述符(已创建并绑定的套接字)。- buf :用于存储接收数据的缓冲区地址。
- len :缓冲区的大小(字节数),避免数据溢出。
- flags :接收方式标志,通常为 0(表示默认方式),也可指定如 MSG_PEEK (查看数据但不清除缓冲区)等。
- src_addr :指向结构体的指针,用于存储发送方的地址信息(如 IP 地址、端口号),若不需要可设为 NULL 。
- addrlen :指向 socklen_t 类型的指针,用于指定 src_addr 缓冲区的大小,函数返回时会更新为实际地址信息的长度。
返回值
- 成功:返回接收的字节数。- 失败:返回 -1,并设置 errno 表示错误原因(如套接字未绑定、连接断开等)。
- 接收到的数据转换为字符串后,通过回调函数 func 处理,得到返回结果。
- sendto :将处理结果发送回客户端(使用 recvfrom 获取的客户端地址)。
sendto 是网络编程中用于发送数据的函数,常用于无连接的 UDP 协议,也可在特定场景下用于 TCP 协议,其核心作用是向指定的目标地址发送数据。

函数原型
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(默认方式),也可指定如 MSG_DONTROUTE (不经过路由表直接发送)等。
- dest_addr :指向结构体的指针,存储目标接收方的地址信息(如 IP 地址、端口号)。
- addrlen : dest_addr 所指向的地址结构体的长度(字节数)。
返回值
- 成功:返回发送的字节数。
- 失败:返回 -1,并设置 errno 表示错误原因(如目标地址无效、套接字未正确初始化等)。
#pragma once#include #include #include #include #include #include #include #include #include #include \"Log.hpp\"// using func_t = std::function;typedef std::function func_t;Log lg;enum{ SOCKET_ERR=1, BIND_ERR};uint16_t defaultport = 8080;std::string defaultip = \"0.0.0.0\";const int size = 1024;class UdpServer{public: UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0), port_(port), ip_(ip),isrunning_(false) {} void Init() { // 1. 创建udp socket sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // PF_INET if(sockfd_ uint32_t 2. uint32_t必须是网络序列的 // ?? // local.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0) { lg(Fatal, \"bind error, errno: %d, err string: %s\", errno, strerror(errno)); exit(BIND_ERR); } lg(Info, \"bind success, errno: %d, err string: %s\", errno, strerror(errno)); } void Run(func_t func) // 对代码进行分层 { isrunning_ = true; char inbuffer[size]; while(isrunning_) { struct sockaddr_in client; socklen_t len = sizeof(client); ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len); if(n 0) close(sockfd_); }private: int sockfd_; // 网路文件描述符 std::string ip_; // 任意地址bind 0 uint16_t port_; // 表明服务器进程的端口号 bool isrunning_;};
这段代码的核心逻辑是:
1. 准备一个包含“IPv4协议、目标端口(网络字节序)、目标IP(网络字节序)”的地址结构;
2. 通过 bind 系统调用将套接字与该地址绑定;
3. 检查绑定结果,失败则退出,成功则进入下一步(接收数据)。
UdpClient.cc (客户端)
主函数核心逻辑
(1)参数解析
- 通过 argc 和 argv 获取命令行参数,提取服务器 IP( serverip )和端口( serverport ),并将端口从字符串转换为整数。
(2)服务器地址结构初始化
- 定义 struct sockaddr_in server 存储服务器地址信息:
- sin_family = AF_INET :指定为 IPv4 协议。
- sin_port = htons(serverport) :将端口从主机字节序转换为网络字节序(大端序),网络通信必须使用网络字节序。
- sin_addr.s_addr = inet_addr(serverip.c_str()) :将字符串格式的 IP 地址转换为网络字节序的整数。
(3)创建 UDP 套接字
- socket(AF_INET, SOCK_DGRAM, 0) :创建套接字, SOCK_DGRAM 表示使用 UDP 协议,返回套接字描述符 sockfd 。
- 若创建失败(返回值 < 0),打印错误信息并退出。
(4)客户端绑定的特殊性
- 代码注释说明:UDP 客户端通常不需要手动调用 bind 绑定端口,而是由操作系统在首次发送数据时自动分配一个随机的空闲端口(保证本机唯一即可),简化了客户端的实现。
(5)循环发送与接收消息
- 发送消息:
- 通过 getline(cin, message) 获取用户输入的消息。
- sendto(sockfd, ...) :向服务器地址发送消息,参数包括套接字、消息缓冲区、长度、服务器地址及地址长度。
- 接收响应:
- 定义 struct sockaddr_in temp 存储发送响应的服务器地址(实际就是目标服务器,此处用于验证)。
- recvfrom(sockfd, ...) :从套接字接收数据,存入 buffer ,同时获取发送方地址(存于 temp )。
- 若接收成功( s > 0 ),在缓冲区末尾添加字符串结束符 \\0 ,并打印服务器响应。
(6)关闭套接字
- 循环结束后(实际因 while(true) 不会主动结束),调用 close(sockfd) 释放资源。
#include #include #include #include #include #include #include #include using namespace std;void Usage(std::string proc){ std::cout << \"\\n\\rUsage: \" << proc << \" serverip serverport\\n\" << std::endl;}// ./udpclient serverip serverportint main(int argc, char *argv[]){ if (argc != 3) { Usage(argv[0]); exit(0); } std::string serverip = argv[1]; uint16_t serverport = std::stoi(argv[2]); struct sockaddr_in server; bzero(&server, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(serverport); //? server.sin_addr.s_addr = inet_addr(serverip.c_str()); socklen_t len = sizeof(server); int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { cout << \"socker error\" << endl; return 1; } // client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择! // 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此! // 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以! // 系统什么时候给我bind呢?首次发送数据的时候 string message; char buffer[1024]; while (true) { cout << \"Please Enter@ \"; getline(cin, message); // std::cout << message < 0) { buffer[s] = 0; cout << buffer << endl; } } close(sockfd); return 0;}
Usage 函数
- 作用:提示用户程序的正确运行方式(需要传入服务器 IP 和端口)。
- 当命令行参数个数不为 3 时(程序名 + 服务器 IP + 服务器端口),调用该函数并退出。



