> 技术文档 > linux_服务端与客户端剖析

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 + 服务器端口),调用该函数并退出。