一、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]);
//创建socket
int 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]; // string
struct 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; //服务器对应IP
bool _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. 创建socket
int 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. clientdone
while(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]; // string
struct 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; // 服务器所对应的IP
bool _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. 创建socket
sockfd = ::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. clientdone
while (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]; // string
struct 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)
{
// v1
auto 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)); // memset
local.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 库的绑定并释放所占用的系统资源。