Linux之Socket 编程 UDP
一、UDP 网络编程
1.1、V1 版本 - echo server
功能:简单的回显服务器和客户端代码
注意:云服务器不允许直接 bind 公有 IP,我们也不推荐编写服务器的时候,bind 明确的 IP,推荐直接写成 INADDR_ANY。
C++
/* Address to accept any incoming messages. */
#define INADDR_ANY ((in_addr_t) 0x00000000)
在网络编程中,当一个进程需要绑定一个网络端口以进行通信时,可以使用INADDR_ANY 作为 IP 地址参数。这样做意味着该端口可以接受来自任何 IP 地址的连 接请求,无论是本地主机还是远程主机。例如,如果服务器有多个网卡(每个网卡上 有不同的 IP 地址),使用 INADDR_ANY 可以省去确定数据是从服务器上具体哪个网 卡/IP 地址上面获取的。
接口介绍:
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
- 参数:
- domain(协议族/地址族),指定通信的协议族,常见选项:
- AF_INET → IPv4 网络协议
- AF_INET6 → IPv6 网络协议
- AF_UNIX 或 AF_LOCAL → 本地进程间通信(Unix Domain Socket)
- type(套接字类型),指定数据传输方式:
- SOCK_STREAM → 面向连接的可靠传输(如TCP,保证数据顺序和可靠性)
- SOCK_DGRAM → 无连接的不可靠传输(如UDP,速度快但不保证顺序和到达)
- SOCK_RAW → 原始套接字(直接访问底层协议,如自定义 IP 报文)
- protocol(具体协议)通常设为 0,表示根据 domain 和 type 自动选择默认协议(如 SOCK_STREAM 默认用 TCP,SOCK_DGRAM 默认用 UDP)。也可显式指定:
- IPPROTO_TCP → TCP
- IPPROTO_UDP → UDP
- 返回值:
- 成功:返回一个 套接字描述符(非负整数,类似文件描述符)。
- 失败:返回 -1,并设置 errno(如 EACCES 权限不足、EINVAL 参数无效等)。
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
- 参数:
- socket:套接字描述符(由 socket() 创建,即socket方法成功后的返回值)
- address:指向要绑定的地址结构体(IPv4 用 struct sockaddr_in,IPv6 用 struct sockaddr_in6)
- address_len:地址结构体的长度(如 sizeof(struct sockaddr_in))
- 返回值:
- 成功:返回 0。
- 失败:返回 -1,并设置 errno。
#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:套接字描述符(需为 SOCK_DGRAM 或 SOCK_RAW 类型)
- buf:接收数据的缓冲区指针
- len:缓冲区的最大容量(字节数)
- flags:控制接收行为的标志(如 MSG_WAITALL、MSG_DONTWAIT,通常设为 0)
- src_addr:可选参数,用于存储发送方的地址信息
- addrlen:输入时指定 src_addr 缓冲区长度,输出时返回实际地址长度
- 返回值:
- 成功:返回接收到的字节数(可能小于 len)。
- 失败:返回 -1,并设置 errno。
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
- 参数:
- sockfd:套接字描述符(需为 SOCK_DGRAM 或 SOCK_RAW 类型)
- buf:待发送数据的缓冲区指针
- len:待发送数据的字节数
- flags:控制发送行为的标志(如 MSG_DONTWAIT、MSG_NOSIGNAL,通常设为 0)
- dest_addr:目标地址结构体(如 struct sockaddr_in)
- addrlen:目标地址结构体的长度(如 sizeof(struct sockaddr_in))
- 返回值:
- 成功:返回实际发送的字节数(可能小于 len)。
- 失败:返回 -1,并设置 errno。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
功能:将32位无符号整数从主机字节序转换为网络字节序(大端序),一般用于IP
参数:hostlong(主机字节序的32位值)
返回值:网络字节序的32位值
uint16_t htons(uint16_t hostshort);
功能:将16位无符号整数从主机字节序转换为网络字节序(大端序),一般用于端口号
参数:hostshort(主机字节序的16位值)
返回值:网络字节序的16位值
uint32_t ntohl(uint32_t netlong);
功能:将32位无符号整数从网络字节序转换回主机字节序,一般用于IP
参数:netlong(网络字节序的32位值)
返回值:主机字节序的32位值
uint16_t ntohs(uint16_t netshort);
功能:将16位无整数从网络字节序转换回主机字节序,一般用于端口号
参数:netshort(网络字节序的16位值)
返回值:主机字节序的16位值
这些函数主要用于解决不同系统间(大端/小端架构)通过网络通信时的字节序兼容性问题。网络协议规定使用大端序作为标准字节序,因此在发送数据前需要用hton函数转换,接收数据后用ntoh函数转换回来。
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
- 功能:将二进制格式的网络地址,即IP(如 IPv4 或 IPv6 地址)转换成可读的字符串格式(点分十进制或十六进制表示)。
- 参数:
- af(Address Family):地址族,指定地址类型:
- AF_INET(IPv4)
- AF_INET6(IPv6)
- src:指向二进制格式的 IP 地址(如 struct in_addr 或 struct in6_addr)。
- dst:存储转换后字符串的缓冲区。该缓冲区由用户自己定义。
- size:缓冲区 dst 的大小(防止溢出)。
- 返回值:
- 成功:返回 dst 的指针(即转换后的字符串)。
- 失败:返回 NULL,并设置 errno。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>in_addr_t inet_addr(const char *cp);
- 功能:将点分十进制格式的 IPv4 地址字符串(如 "192.168.1.1")转换为 32 位网络字节序(大端序)的二进制值(in_addr_t 类型)。
参数:cp:点分十进制格式的 IPv4 地址字符串(如 "192.168.1.1")。
- 返回值:
- 成功:返回 网络字节序的 32 位无符号整数(in_addr_t)。
- 失败(如非法格式):返回 INADDR_NONE(通常是 0xFFFFFFFF)。
char *inet_ntoa(struct in_addr in);
- 功能:将 32 位网络字节序的 IPv4 地址(struct in_addr)转换为 点分十进制字符串(如 "192.168.1.1")。
- 参数:in:struct in_addr 结构体,包含一个网络字节序的 IPv4 地址。
- 返回值:
- 返回 静态分配的字符串指针(存储在静态缓冲区中)。
- 非线程安全(多次调用可能会覆盖之前的结果)。
示例代码:(日志和锁使用线程同步与互斥博文中示例代码中封装的, 即Log.hpp, Mutex.hpp)
Common.hpp:
#pragma once#include <iostream>#define Die(code) do{ \exit(code); \
}while(0) \#define CONV(v) (struct sockaddr *)(v)enum
{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR
};
InetAddr.hpp:
#pragma once#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"class InetAddr
{
private:void PortNet2Host(){_port = ::ntohs(_net_addr.sin_port);}void IpNet2Host(){char ipbuffer[64];const char *ip = ::inet_ntop(AF_INET, &_net_addr.sin_addr, ipbuffer, sizeof(ipbuffer));(void)ip;}
public:InetAddr(){}InetAddr(const struct sockaddr_in &addr): _net_addr(addr){PortNet2Host();IpNet2Host();}InetAddr(uint16_t port):_port(port),_ip(""){_net_addr.sin_family = AF_INET;_net_addr.sin_port = ::htons(_port);_net_addr.sin_addr.s_addr = INADDR_ANY;}struct sockaddr *NetAddr(){ return CONV(&_net_addr); }socklen_t NetAddrLen(){ return sizeof(_net_addr); }std::string Ip(){ return _ip; }uint16_t Port(){ return _port; }~InetAddr(){}private:struct sockaddr_in _net_addr;std::string _ip;uint16_t _port;
};
UdpClientMain.cc:
#include "Common.hpp"
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>//CS模式
int main(int argc, char *argv[])
{if(argc != 3){std::cerr << "Usage: " << argv[0] << "serverip serverport" << std::endl;Die(USAGE_ERR);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);//创建socketint sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if(sockfd < 0){std::cerr << "socket error" << std::endl;Die(SOCKET_ERR);}//填充server信息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());//开始通信while(true){std::cout << "Please Enter# ";std::string message;std::getline(std::cin, message);// client必须也要有自己的ip和端口!但是客户端,不需要自己显示的调用bind!!// 客户端首次sendto消息的时候,由OS自动进行bind// 1. 为什么client自动随机bind端口号? 一个端口号,只能被一个进程bind,防止用户自己绑定导致端口冲突,进而无法启动应用// 2. 为什么server要显示的bind?服务器的端口号,必须稳定!!必须是众所周知且不能改变轻易改变的! 一般显示绑定一个固定的int n = ::sendto(sockfd, message.c_str(), message.size(), 0, CONV(&server), sizeof(server));(void)n;struct sockaddr_in temp;socklen_t len = sizeof(temp);char buffer[1024];n = ::recvfrom(sockfd, buffer, sizeof(buffer), 0, CONV(&temp), &len);if(n > 0){buffer[n] = 0;std::cout << buffer << std::endl;}}return 0;
}
UdpServer.hpp:
#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cerrno>
#include <strings.h>#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "InetAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"using namespace LogModule;const static int gsockfd = -1;
// const static std::string gdefaultip = "127.0.0.1"; // 表示本地主机
const static uint16_t gdefaultport = 8080;class UdpServer
{
public:UdpServer(uint16_t port = gdefaultport):_sockfd(gsockfd),_addr(port),_isrunning(false){}void InitServer(){//创建socket_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if(_sockfd < 0){LOG(LogLevel::FATAL) << "socket: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success, sockfd is : "<< _sockfd;// 2. 填充网络信息,并bind绑定// 2.1 有没有把socket信息,设置进入内核中??没有,只是填充了结构体!// 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()); // 1. string ip->4bytes 2. network order //TODO// local.sin_addr.s_addr = INADDR_ANY;//bind:设置进入内核中int n = ::bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());if (n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success";}void Start(){_isrunning = true;while (true){char inbuffer[1024]; // stringstruct sockaddr_in peer;socklen_t len = sizeof(peer); // 必须设定ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &len);if (n > 0){// 1. 消息内容 && 2. 谁发给我的// uint16_t clientport = ::ntohs(peer.sin_port);// std::string clientip = ::inet_ntoa(peer.sin_addr);InetAddr cli(peer);inbuffer[n] = 0;std::string clientinfo = cli.Ip() + ":" + std::to_string(cli.Port()) + " # " + inbuffer;LOG(LogLevel::DEBUG) << clientinfo;std::string echo_string = "echo# ";echo_string += inbuffer;::sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, CONV(&peer), sizeof(peer));}}_isrunning = false;}~UdpServer(){if (_sockfd > gsockfd)::close(_sockfd);}private:int _sockfd;InetAddr _addr;// uint16_t _port;//服务器端口号// std::string _ip; //服务器对应IPbool _isrunning;
};#endif
UdpServerMain.cc:
#include "UdpServer.hpp"// ./server_udp localport
int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << " localport" << std::endl;Die(USAGE_ERR);}// std::string ip = argv[1];uint16_t port = std::stoi(argv[1]);ENABLE_CONSOLE_LOG();std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(port);svr_uptr->InitServer();svr_uptr->Start();return 0;
}
注意:
- 云服务器不允许我们绑定公网IP。
- 正常情况下,一个服务器可能会接受到不同IP的客户端的信息,但是服务器一旦绑定了某一个IP后就只能接受这一个IP发来的信息,所以正常情况下服务器的代码中不要绑定具体的IP地址。而是像示例代码一样,可以接受任意IP的信息。
1.2、V2 版本 - DictServer
功能:实现一个简单的英译汉的网络字典
示例代码:(日志和锁使用线程同步与互斥博文中示例代码中封装的, 即Log.hpp, Mutex.hpp,这里不在展示)
Common.hpp:
#pragma once#include <iostream>
#include <string>#define Die(code) \do \{ \exit(code); \} while (0)#define CONV(v) (struct sockaddr *)(v)enum
{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR
};// happy: 快乐的
bool SplitString(const std::string &line, std::string *key, std::string *value, const std::string &sep)
{auto pos = line.find(sep);if(pos == std::string::npos) return false;*key = line.substr(0, pos);*value = line.substr(pos+sep.size());if(key->empty() || value->empty()) return false;return true;
}
Dictionary.hpp:
#pragma once#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"
#include "Common.hpp"const std::string gpath = "./";
const std::string gdictname = "dict.txt";
const std::string gsep = ": ";using namespace LogModule;class Dictionary
{
private:bool LoadDictionary(){std::string file = _path + _filename;std::ifstream in(file.c_str());if (!in.is_open()){LOG(LogLevel::ERROR) << "open file " << file << " error";return false;}std::string line;while (std::getline(in, line)){// happy: 快乐的std::string key;std::string value;if (SplitString(line, &key, &value, gsep)){ // line -> key, value_dictionary.insert(std::make_pair(key, value));}}in.close();return true;}public:Dictionary(const std::string &path = gpath, const std::string &filename = gdictname): _path(path),_filename(filename){LoadDictionary();Print();}std::string Translate(const std::string &word){auto iter = _dictionary.find(word);if(iter == _dictionary.end()) return "None";return iter->second;}void Print(){for(auto &item : _dictionary){std::cout << item.first << ":" << item.second<< std::endl;}}~Dictionary(){}private:std::unordered_map<std::string, std::string> _dictionary;std::string _path;std::string _filename;
};
dict.txt:
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
InetAddr.hpp:
#pragma once#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"class InetAddr
{
private:void PortNet2Host(){_port = ::ntohs(_net_addr.sin_port);}void IpNet2Host(){char ipbuffer[64];const char *ip = ::inet_ntop(AF_INET, &_net_addr.sin_addr, ipbuffer, sizeof(ipbuffer));(void)ip;}public:InetAddr(){}InetAddr(const struct sockaddr_in &addr) : _net_addr(addr){PortNet2Host();IpNet2Host();}InetAddr(uint16_t port) : _port(port), _ip(""){_net_addr.sin_family = AF_INET;_net_addr.sin_port = htons(_port);_net_addr.sin_addr.s_addr = INADDR_ANY;}struct sockaddr *NetAddr() { return CONV(&_net_addr); }socklen_t NetAddrLen() { return sizeof(_net_addr); }std::string Ip() { return _ip; }uint16_t Port() { return _port; }~InetAddr(){}private:struct sockaddr_in _net_addr;std::string _ip;uint16_t _port;
};
UdpClientMain.cc:
#include "Common.hpp"
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>// CS
// ./client_udp serverip serverport
int main(int argc, char *argv[])
{if(argc != 3){std::cerr << "Usage: " << argv[0] << " serverip serverport" << std::endl;Die(USAGE_ERR);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 1. 创建socketint sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if(sockfd < 0){std::cerr << "socket error" << std::endl;Die(SOCKET_ERR);}// 1.1 填充server信息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());// 2. clientdonewhile(true){std::cout << "Please Enter# ";std::string message;std::getline(std::cin, message);int n = ::sendto(sockfd, message.c_str(),message.size(), 0, CONV(&server), sizeof(server));(void)n;struct sockaddr_in temp;socklen_t len = sizeof(temp);char buffer[1024];n = ::recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, CONV(&temp), &len);if(n > 0){buffer[n] = 0;std::cout << buffer << std::endl;}}return 0;
}
UdpServer.hpp:
#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__#include <iostream>
#include <string>
#include <memory>
#include <functional>
#include <cstring>
#include <cerrno>
#include <strings.h>#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "InetAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"using namespace LogModule;const static int gsockfd = -1;
// const static std::string gdefaultip = "127.0.0.1"; // 表示本地主机
const static uint16_t gdefaultport = 8080;using func_t = std::function<std::string(const std::string&)>;class UdpServer
{
public:UdpServer(func_t func, uint16_t port = gdefaultport): _sockfd(gsockfd),_addr(port),_isrunning(false),_func(func){}// 都是套路void InitServer(){// 1. 创建socket_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0); // IP?PORT?网络?本地?if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success, sockfd is : " << _sockfd;// 2. 填充网络信息,并bind绑定// 2.1 有没有把socket信息,设置进入内核中??没有,只是填充了结构体!// 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()); // 1. string ip->4bytes 2. network order //TODO// local.sin_addr.s_addr = INADDR_ANY;// 2.2 bind : 设置进入内核中int n = ::bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());if (n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success";}void Start(){_isrunning = true;while (true){char inbuffer[1024]; // stringstruct sockaddr_in peer;socklen_t len = sizeof(peer); // 必须设定ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &len);if (n > 0){// 1. 消息内容 && 2. 谁发给我的// uint16_t clientport = ::ntohs(peer.sin_port);// std::string clientip = ::inet_ntoa(peer.sin_addr);// InetAddr cli(peer);// inbuffer[n] = 0;// std::string clientinfo = cli.Ip() + ":" + std::to_string(cli.Port()) + " # " + inbuffer;// LOG(LogLevel::DEBUG) << clientinfo;// std::string echo_string = "echo# ";// echo_string += inbuffer;// 把英文单词转化成为中文inbuffer[n] = 0;std::string result = _func(inbuffer); // 这个是回调,出去,还会回来!!!!::sendto(_sockfd, result.c_str(), result.size(), 0, CONV(&peer), sizeof(peer));}}_isrunning = false;}~UdpServer(){if(_sockfd > gsockfd)::close(_sockfd);}private:int _sockfd;InetAddr _addr;// uint16_t _port; // 服务器未来的端口号// // std::string _ip; // 服务器所对应的IPbool _isrunning; // 服务器运行状态// 业务,回调func_t _func;
};#endif
UdpServerMain.cc:
#include "UdpServer.hpp"
#include "Dictionary.hpp"// ./server_udp localport
int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << " localport" << std::endl;Die(USAGE_ERR);}// std::string ip = argv[1];uint16_t port = std::stoi(argv[1]);ENABLE_CONSOLE_LOG();std::shared_ptr<Dictionary> dict_sptr = std::make_shared<Dictionary>();std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>([&dict_sptr](const std::string &word){std::cout << "|" << word << "|" << std::endl;return dict_sptr->Translate(word);}, port);// std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(std::bind(&Dictionary::Translate,\// dict_sptr.get(), std::placeholders::_1), port);svr_uptr->InitServer();svr_uptr->Start();return 0;
}
1.3、V3 版本 - 简单聊天室
UDP 协议支持全双工,一个 sockfd,既可以读取,又可以写入,对于客户端和服务端同样如此,多线程客户端,同时读取和写入。
示例代码:(Cond.hpp,Log.hpp,Mutex.hpp,Thread.hpp,ThreadPool.hpp在以前的文章中都有封装过,这里就不重复展示了)
Common.hpp:
#pragma once#include <iostream>#define Die(code) \do \{ \exit(code); \} while (0)#define CONV(v) (struct sockaddr *)(v)enum
{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR
};
InetAddr.hpp:
#pragma once#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"class InetAddr
{
private:void PortNet2Host(){_port = ::ntohs(_net_addr.sin_port);}void IpNet2Host(){char ipbuffer[64];const char *ip = ::inet_ntop(AF_INET, &_net_addr.sin_addr, ipbuffer, sizeof(ipbuffer));(void)ip;_ip = ipbuffer;}public:InetAddr(){}InetAddr(const struct sockaddr_in &addr) : _net_addr(addr){PortNet2Host();IpNet2Host();}bool operator == (const InetAddr &addr){return _ip == addr._ip && _port == addr._port; //debug//return _ip == addr._ip; //debug}InetAddr(uint16_t port) : _port(port), _ip(""){_net_addr.sin_family = AF_INET;_net_addr.sin_port = htons(_port);_net_addr.sin_addr.s_addr = INADDR_ANY;}struct sockaddr *NetAddr() { return CONV(&_net_addr); }socklen_t NetAddrLen() { return sizeof(_net_addr); }std::string Ip() { return _ip; }uint16_t Port() { return _port; }std::string Addr(){return Ip() + ":" + std::to_string(Port());}~InetAddr(){}private:struct sockaddr_in _net_addr;std::string _ip;uint16_t _port;
};
UdpClientMain.cc:
#include "Common.hpp"
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <signal.h>int sockfd = -1;
struct sockaddr_in server;void ClientQuit(int signo)
{(void)signo;const std::string quit = "QUIT";int n = ::sendto(sockfd, quit.c_str(), quit.size(), 0, CONV(&server), sizeof(server));exit(0);
}void *Recver(void *args)
{while (true){(void)args;struct sockaddr_in temp;socklen_t len = sizeof(temp);char buffer[1024];int n = ::recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&temp), &len);if (n > 0){buffer[n] = 0;std::cerr << buffer << std::endl; // 代码没问题,重定向也没问题,管道读写同时打开,才会继续向后运行// fprintf(stderr, "%s\n", buffer);// fflush(stderr);}}
}// CS
// ./client_udp serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " serverip serverport" << std::endl;Die(USAGE_ERR);}signal(2, ClientQuit);std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 1. 创建socketsockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;Die(SOCKET_ERR);}// 1.1 填充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());pthread_t tid;pthread_create(&tid, nullptr, Recver, nullptr);// 1.2 启动的时候,给服务器推送消息即可const std::string online = " ... 来了哈!";int n = ::sendto(sockfd, online.c_str(), online.size(), 0, CONV(&server), sizeof(server));// 2. clientdonewhile (true){std::cout << "Please Enter# ";std::string message;std::getline(std::cin, message);int n = ::sendto(sockfd, message.c_str(), message.size(), 0, CONV(&server), sizeof(server));(void)n;}return 0;
}
UdpServer.hpp:
#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cerrno>
#include <strings.h>#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>#include "InetAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"
#include "ThreadPool.hpp"using namespace LogModule;
using namespace ThreadPoolModule;const static int gsockfd = -1;
// const static std::string gdefaultip = "127.0.0.1"; // 表示本地主机
const static uint16_t gdefaultport = 8080;using adduser_t = std::function<void(InetAddr &id)>;
using remove_t = std::function<void(InetAddr &id)>;using task_t = std::function<void()>;
using route_t = std::function<void(int sockfd, const std::string &message)>;class nocopy
{
public:nocopy(){}nocopy(const nocopy &) = delete;const nocopy& operator = (const nocopy &) = delete;~nocopy(){}
};class UdpServer : public nocopy
{
public:UdpServer(uint16_t port = gdefaultport): _sockfd(gsockfd),_addr(port),_isrunning(false){}// 都是套路void InitServer(){// 1. 创建socket_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0); // IP?PORT?网络?本地?if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success, sockfd is : " << _sockfd;// //local.sin_addr.s_addr = ::inet_addr(_ip.c_str()); // 1. string ip->4bytes 2. network order //TODO// local.sin_addr.s_addr = INADDR_ANY;// 2.2 bind : 设置进入内核中int n = ::bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());if (n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success";}void RegisterService(adduser_t adduser, route_t route, remove_t removeuser){_adduser = adduser;_route = route;_removeuser = removeuser;}void Start(){_isrunning = true;while (true){char inbuffer[1024]; // stringstruct sockaddr_in peer;socklen_t len = sizeof(peer); // 必须设定ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &len);if (n > 0){// 1. 消息内容 && 2. 谁发给我的// uint16_t clientport = ::ntohs(peer.sin_port);// std::string clientip = ::inet_ntoa(peer.sin_addr);InetAddr cli(peer);inbuffer[n] = 0;std::string message;if (strcmp(inbuffer, "QUIT") == 0){// 移除观察者_removeuser(cli);message = cli.Addr() + "# " + "我走了,你们聊!";}else{// 2. 新增用户_adduser(cli);message = cli.Addr() + "# " + inbuffer;}// 3. 构建转发任务,推送给线程池,让线程池进行转发task_t task = std::bind(UdpServer::_route, _sockfd, message);ThreadPool<task_t>::getInstance()->Equeue(task);// std::string clientinfo = cli.Ip() + ":" + std::to_string(cli.Port()) + " # " + inbuffer;// LOG(LogLevel::DEBUG) << clientinfo;// std::string echo_string = "echo# ";// echo_string += inbuffer;// ::sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, CONV(&peer), sizeof(peer));}}_isrunning = false;}~UdpServer(){if (_sockfd > gsockfd)::close(_sockfd);}private:int _sockfd;InetAddr _addr;bool _isrunning; // 服务器运行状态// 新增用户adduser_t _adduser;// 移除用户remove_t _removeuser;// 数据转发route_t _route;
};#endif
UdpServerMain.cc:
#include "UdpServer.hpp"
#include "User.hpp"// ./server_udp localport
int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << " localport" << std::endl;Die(USAGE_ERR);}// std::string ip = argv[1];uint16_t port = std::stoi(argv[1]);ENABLE_CONSOLE_LOG();// 用户管理模块std::shared_ptr<UserManager> um = std::make_shared<UserManager>();// 网络模块std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(port);svr_uptr->RegisterService([&um](InetAddr &id){ um->AddUser(id); },[&um](int sockfd, const std::string &message){ um->Router(sockfd, message);},[&um](InetAddr &id){ um->DelUser(id);});svr_uptr->InitServer();svr_uptr->Start();return 0;
}
User.hpp:
#pragma once#include <iostream>
#include <string>
#include <list>
#include <memory>
#include <algorithm>
#include <sys/types.h>
#include <sys/socket.h>#include "InetAddr.hpp"
#include "Log.hpp"
#include "Mutex.hpp"using namespace LogModule;
using namespace LockModule;class UserInterface
{
public:virtual ~UserInterface() = default;virtual void SendTo(int sockfd, const std::string &message) = 0;virtual bool operator==(const InetAddr &u) = 0;virtual std::string Id() = 0;
};class User : public UserInterface
{
public:User(const InetAddr &id) : _id(id){}void SendTo(int sockfd, const std::string &message) override{LOG(LogLevel::DEBUG) << "send message to " << _id.Addr() << " info: " << message;int n = ::sendto(sockfd, message.c_str(), message.size(), 0, _id.NetAddr(), _id.NetAddrLen());(void)n;}bool operator==(const InetAddr &u) override{return _id == u;}std::string Id() override{return _id.Addr();}~User(){}private:InetAddr _id;
};// 对用户消息进行路由
// UserManager 把所有的用户先管理起来!
// 观察者模式!observer
class UserManager
{
public:UserManager(){}void AddUser(InetAddr &id){LockGuard lockguard(_mutex);for (auto &user : _online_user){if (*user == id){LOG(LogLevel::INFO) << id.Addr() << "用户已经存在";return;}}LOG(LogLevel::INFO) << " 新增该用户: " << id.Addr();_online_user.push_back(std::make_shared<User>(id));PrintUser();}void DelUser(InetAddr &id){// v1auto pos = std::remove_if(_online_user.begin(), _online_user.end(), [&id](std::shared_ptr<UserInterface> &user){return *user == id;});_online_user.erase(pos, _online_user.end());PrintUser();//v2// for(auto user : _online_user)// {// if(*user == id)// {// _online_user.erase(user);// break; // 迭代器失效的问题// }// }}void Router(int sockfd, const std::string &message){LockGuard lockguard(_mutex);for (auto &user : _online_user){user->SendTo(sockfd, message);}}void PrintUser(){for(auto user : _online_user){LOG(LogLevel::DEBUG) <<"在线用户-> "<< user->Id();}}~UserManager(){}private:std::list<std::shared_ptr<UserInterface>> _online_user;Mutex _mutex;
};
1.4、补充内容
1.4.1、地址转换函数
本节只介绍基于 IPv4 的 socket 网络编程,sockaddr_in 中的成员 sin_addr(struct in_addr 类型) 表示 32 位 的 IP 地址。但是我们通常用点分十进制的字符串表示 IP 地址,以下函数可以在字符串表示和 in_addr 表示之间转换。字符串转 in_addr 的函数:
in_addr 转字符串的函数:
其中 inet_pton 和 inet_ntop 不仅可以转换 IPv4 的 in_addr,还可以转换 IPv6 的in6_addr,因此函数接口是 void *addrptr。
1.4.2、inet_ntoa
inet_ntoa 这个函数返回了一个 char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存 ip 的结果. 那么是否需要调用者手动释放呢?
man 手册上说, inet_ntoa 函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放。但是这也导致了它是非线程安全的。因为 inet_ntoa 把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果。
如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
在 APUE 中, 明确提出 inet_ntoa 不是线程安全的函数。但是在 centos7 上测试, 并没有出现问题, 可能内部的实现加了互斥锁。在多线程环境下, 推荐使用 inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题。
二、网络命令
2.1、Ping 命令
功能:ping(Packet Internet Groper)是一种基础的 网络诊断工具,用于测试主机之间的 连通性 和 网络延迟。它通过发送 ICMP Echo Request 数据包到目标主机,并等待对方返回 ICMP Echo Reply 来检测网络是否通畅。
语法:ping [目标IP或域名]
使用:
ping 8.8.8.8 # 测试与 Google DNS 的连通性
ping www.baidu.com # 测试与百度的连通性
注意:ping命名一旦发送,默认会一直检测,不会停下。
选项:
- -c [次数] 指定发送的请求次数 ping -c 4 8.8.8.8 只检测四次。
- -i [秒数] 设置发送间隔(默认 1 秒) ping -i 0.5 8.8.8.8
2.2、netstat
netstat 是一个用来查看网络状态的重要工具。
语法:netstat [选项]
功能:查看网络状态
常用选项:
- n 拒绝显示别名,能显示数字的全部转化成数字
- l 仅列出有在 Listen (监听) 的服務状态
- p 显示建立相关链接的程序名,即展示相关进程信息
- t (tcp)仅显示 tcp 相关选项
- u (udp)仅显示 udp 相关选项
- a (all)显示所有选项,默认不显示 LISTEN 相关
使用:
常规使用:netstat -nuap
// 每个 1s 执行一次 netstat -nltp
watch -n 1 netstat -nltp
2.3、pidof
在查看服务器的进程 id 时非常方便。
语法:pidof [进程名]
功能:通过进程名,查看进程 id
三、验证 UDP - windows 作为 client 访问 Linux
注意:一定要开放云服务器对应的端口号,在你的阿里云或者腾讯云或者华为云的网站后台中开放。
UdpClient.cc:(Windows端)
#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
#include <WinSock2.h>
#include <Windows.h>#pragma warning(disable : 4996)#pragma comment(lib, "ws2_32.lib")std::string serverip = ""; // 填写你的云服务器ip
uint16_t serverport = 8888; // 填写你的云服务开放的端口号int main()
{WSADATA wsd;WSAStartup(MAKEWORD(2, 2), &wsd);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());SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd == SOCKET_ERROR){std::cout << "socker error" << std::endl;return 1;}std::string message;char buffer[1024];while (true){std::cout << "Please Enter@ ";std::getline(std::cin, message);if(message.empty()) continue;sendto(sockfd, message.c_str(), (int)message.size(), 0, (struct sockaddr *)&server, sizeof(server));struct sockaddr_in temp;int len = sizeof(temp);int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len);if (s > 0){buffer[s] = 0;std::cout << buffer << std::endl;}}closesocket(sockfd);WSACleanup();return 0;
}
UdpServer.cc:(Linux端)
#include <iostream>
#include <string>
#include <memory>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>const static uint16_t defaultport = 8888;
const static int defaultfd = -1;
const static int defaultsize = 1024;enum
{Usage_Err = 1,Socket_Err,Bind_Err
};class UdpServer
{
public:UdpServer(uint16_t port = defaultport): _port(port), _sockfd(defaultfd){}void Init(){// 1. 创建socket,就是创建了文件细节_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){exit(Socket_Err);}// 2. 绑定,指定网络信息struct sockaddr_in local;bzero(&local, sizeof(local)); // memsetlocal.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY; // 1. 4字节IP 2. 变成网络序列// 结构体填完,设置到内核中了吗??没有int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n != 0){exit(Bind_Err);}}void Start(){// 服务器永远不退出char buffer[defaultsize];for (;;){struct sockaddr_in peer;socklen_t len = sizeof(peer); // 不能乱写ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){uint16_t clientport = ntohs(peer.sin_port);std::string clientip = inet_ntoa(peer.sin_addr);std::string prefix = clientip + ":" + std::to_string(clientport);buffer[n] = 0;std::cout << prefix << "# " << buffer << std::endl;std::string echo = buffer;echo += "[udp server echo message]";sendto(_sockfd, echo.c_str(), echo.size(), 0, (struct sockaddr *)&peer, len);}}}~UdpServer(){}private:uint16_t _port;int _sockfd;
};void Usage(std::string proc)
{std::cout << "Usage : \n\t" << proc << " local_port\n"<< std::endl;
}// ./udp_server 8888
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);return Usage_Err;}uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);usvr->Init();usvr->Start();return 0;
}
代码运行后可以验证 udp tcpclient(Windows)和 tcpserver(Linux)可以通信。这里就不演示了,下面是Windows端代码的一些解释:
C++:
WinSock2.h 是 Windows Sockets API(应用程序接口)的头文件,用于在Windows 平台上进行网络编程。它包含了 Windows Sockets 2(Winsock2)所需的数据类型、函数声明和结构定义,使得开发者能够创建和使用套接字 (sockets)进行网络通信。
在编写使用 Winsock2 的程序时,需要在源文件中包含 WinSock2.h 头文件。这 样,编译器就能够识别并理解 Winsock2 中定义的数据类型和函数,从而能够正确地编译和链接网络相关的代码。
此外,与 WinSock2.h 头文件相对应的是 ws2_32.lib 库文件。在链接阶段,需要 将这个库文件链接到程序中,以确保运行时能够找到并调用 Winsock2 API 中实现的函数。
在 WinSock2.h 中定义了一些重要的数据类型和函数,如:
WSADATA:保存初始化 Winsock 库时返回的信息。
SOCKET:表示一个套接字描述符,用于在网络中唯一标识一个套接字。
sockaddr_in:IPv4 地址结构体,用于存储 IP 地址和端口号等信息。
socket():创建一个新的套接字。
bind():将套接字与本地地址绑定。
listen():将套接字设置为监听模式,等待客户端的连接请求。
accept():接受客户端的连接请求,并返回一个新的套接字描述符,用于与客户端 进行通信。
C++:
WSAStartup 函数是 Windows Sockets API 的初始化函数,它用于初始化Winsock 库。该函数在应用程序或 DLL 调用任何 Windows 套接字函数之前必须首 先执行,它扮演着初始化的角色。
以下是 WSAStartup 函数的一些关键点:
它接受两个参数:wVersionRequested 和 lpWSAData。wVersionRequested 用于 指定所请求的 Winsock 版本,通常使用 MAKEWORD(major, minor)宏,其中major 和 minor 分别表示请求的主版本号和次版本号。lpWSAData 是一个指向WSADATA 结构的指针,用于接收初始化信息。
如果函数调用成功,它会返回 0;否则,返回错误代码。
WSAStartup 函数的主要作用是向操作系统说明我们将使用哪个版本的 Winsock
库,从而使得该库文件能与当前的操作系统协同工作。成功调用该函数后,Winsock 库的状态会被初始化,应用程序就可以使用 Winsock 提供的一系列套接字 服务,如地址家族识别、地址转换、名字查询和连接控制等。这些服务使得应用程 序可以与底层的网络协议栈进行交互,实现网络通信。
在调用 WSAStartup 函数后,如果应用程序完成了对请求的 Socket 库的使用,应调用 WSACleanup 函数来解除与 Socket 库的绑定并释放所占用的系统资源。