文章目录
- [1. Echo server](#1. Echo server)
-
- [1.1 功能介绍](#1.1 功能介绍)
- [1.2 Socket 系统调用](#1.2 Socket 系统调用)
-
- [1.2.1 socket():创建套接字文件描述符](#1.2.1 socket():创建套接字文件描述符)
- [1.2.2 bind():绑定地址和端口](#1.2.2 bind():绑定地址和端口)
- [1.2.3 recvfrom():UDP 接收数据](#1.2.3 recvfrom():UDP 接收数据)
- [1.2.4 sendto():UDP 发送数据](#1.2.4 sendto():UDP 发送数据)
- [1.2.5 close():关闭套接字](#1.2.5 close():关闭套接字)
- [1.2.6 辅助函数(网络字节序转换)](#1.2.6 辅助函数(网络字节序转换))
- [1.3 demo v1](#1.3 demo v1)
-
- [1.3.1 源码](#1.3.1 源码)
- [1.3.2 实验结果](#1.3.2 实验结果)
- [1.3.3 流程详解](#1.3.3 流程详解)
- [1.4 demo v2](#1.4 demo v2)
- [2. DictServer](#2. DictServer)
-
- [2.1 功能介绍](#2.1 功能介绍)
- [2.2 源码](#2.2 源码)
- [2.3 实验结果](#2.3 实验结果)
- [3. 简单聊天室](#3. 简单聊天室)
-
- [3.1 源码](#3.1 源码)
本文主要是关于UDP网络通信编程的三个demo,通过这三个demo完全掌握udp网络通信。
1. Echo server
1.1 功能介绍
功能:简单的回显服务器和客戶端代码,客户端向服务器发送消息,服务器接受消息之后向客户端回显。
解释:写三个文件,将来分别生成两个可执行程序,客户端和服务端。
客户端向服务器发送消息,服务端接受消息,然后回显给客户端输入的内容。
1.2 Socket 系统调用
网络通信就要打开网络文件,打开方式: socket套接字
Q:啥是套接字?套接字是干啥用的?
A:
套接字 = ip + port
Socket 是操作系统分配的特殊文件描述符(文件指针),应用程序通过读写这个"文件",
操作系统会把数据交给网卡,送到目标主机的「IP + 端口」对应的 Socket(文件描述符)指向的文件中;
接收方的进程也通过自己的Socket(文件描述符)读取数据。
Socket 套接字的系统调用是操作系统提供的网络编程接口(POSIX 标准),主要用于实现进程间的网络通信(跨主机/本机)。
1.2.1 socket():创建套接字文件描述符
功能
创建一个套接字(本质是一个文件描述符),是所有网络编程的第一步,用于后续的网络通信。
函数原型

c
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数解析
| 参数 | 说明 |
|---|---|
domain |
地址族(协议族):表明通信方式 - AF_INET:IPv4 网络协议(最常用) - AF_INET6:IPv6 - AF_UNIX:本地进程通信 |
type |
套接字类型: - SOCK_DGRAM:UDP(无连接、不可靠、面向数据报) - SOCK_STREAM:TCP(有连接、可靠、面向字节流) |
protocol |
具体协议: - 0:根据 domain+type 自动选择(UDP/TCP 填 0 即可) - IPPROTO_UDP/IPPROTO_TCP:显式指定 |
返回值
- 成功:返回一个非负整数(套接字文件描述符,类似文件的 fd);
- 失败:返回 -1,并设置
errno(可通过perror()打印错误)。
示例(UDP 场景)
cpp
// 创建 IPv4 + UDP 套接字
int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_fd < 0) {
perror("socket create failed"); // 打印错误信息
return -1;
}
1.2.2 bind():绑定地址和端口
功能
将创建的套接字与本地 IP 地址 + 端口号绑定,让操作系统知道该套接字要监听哪个端口的网络数据。
- UDP 服务器必须绑定端口(否则客户端无法定位服务器);
- UDP 客户端一般不绑定(系统自动分配临时端口)。
函数原型

c
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数解析
| 参数 | 说明 |
|---|---|
sockfd |
socket() 返回的套接字文件描述符 |
addr |
指向 sockaddr 结构体的指针(实际用 sockaddr_in(IPv4)强转) |
addrlen |
addr 结构体的长度(sizeof(struct sockaddr_in)) |
关键结构体:sockaddr_in(IPv4 地址)
cpp
#include <netinet/in.h>
struct sockaddr_in {
sa_family_t sin_family; // 协议族,固定为 AF_INET,表示用哪个协议
in_port_t sin_port; // 端口号(需转网络字节序:htons())
struct in_addr sin_addr; // IP 地址(in_addr 仅包含一个 uint32_t 成员 s_addr)
};
// 常用 IP 地址设置:
// - INADDR_ANY:绑定所有本机网卡(0.0.0.0),接收任意网卡的数据包
// - inet_addr("192.168.1.100"):绑定指定 IP
返回值
- 成功:返回 0;
- 失败:返回 -1,并设置
errno(常见错误:端口被占用 →EADDRINUSE)。
示例(UDP 服务器绑定)
cpp
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr)); // 初始化结构体
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡
server_addr.sin_port = htons(8080); // 端口转网络字节序(主机→网络)
// 绑定
if (bind(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
close(sock_fd);
return -1;
}
1.2.3 recvfrom():UDP 接收数据
功能
从 UDP 套接字接收数据,并获取发送方的 IP 地址和端口(UDP 无连接,每次接收都要知道对方地址才能回复)。
函数原型
c
#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 |
接收数据的缓冲区(char 数组) |
len |
缓冲区长度(注意留 1 字节给 '\0',避免字符串越界) |
flags |
接收标志:0(阻塞接收,默认);MSG_DONTWAIT(非阻塞) |
src_addr |
输出参数:存储发送方(客户端)的地址结构体(sockaddr_in) |
addrlen |
输入输出参数:传入 sizeof(sockaddr_in),返回实际地址长度 |
返回值
- 成功:返回接收到的字节数;
- 失败:返回 -1(如中断 →
EINTR,可重试); - 0:无实际意义(UDP 无连接,不会返回 0)。
示例
cpp
char buffer[1024] = {0};
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
// 阻塞接收数据
ssize_t recv_len = recvfrom(sock_fd, buffer, sizeof(buffer)-1, 0,
(struct sockaddr*)&client_addr, &client_len);
if (recv_len < 0) {
perror("recvfrom failed");
continue;
}
buffer[recv_len] = '\0'; // 确保字符串结束
// 转换客户端 IP 为字符串(网络字节序→点分十进制)
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
std::cout << "收到 " << client_ip << ":" << ntohs(client_addr.sin_port)
<< " 的数据:" << buffer << std::endl;
1.2.4 sendto():UDP 发送数据
功能
向指定的 IP + 端口发送 UDP 数据(UDP 无连接,每次发送都要指定目标地址)。
函数原型
c
#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 |
要发送的数据长度(strlen(buf)) |
flags |
发送标志:0(默认) |
dest_addr |
目标地址结构体(客户端/服务器的 sockaddr_in) |
addrlen |
目标地址结构体长度(sizeof(sockaddr_in)) |
返回值
- 成功:返回发送的字节数;
- 失败:返回 -1,并设置
errno。
示例(回复客户端)
cpp
const char* reply = "收到你的消息!";
// 用 recvfrom 获取的 client_addr 作为目标地址
ssize_t send_len = sendto(sock_fd, reply, strlen(reply), 0,
(struct sockaddr*)&client_addr, client_len);
if (send_len < 0) {
perror("sendto failed");
}
1.2.5 close():关闭套接字
功能
关闭套接字文件描述符,释放系统资源(如端口、文件描述符)。
- 必须调用,否则会导致文件描述符泄漏;
unique_ptr管理的对象析构时调用close()是最佳实践(如你之前的 UdpServer 析构函数)。
函数原型
c
#include <unistd.h>
int close(int fd);
参数/返回值
fd:套接字文件描述符;- 成功返回 0,失败返回 -1。
示例
cpp
~UdpServer() {
if (sock_fd_ != -1) {
close(sock_fd_); // 析构时自动关闭
sock_fd_ = -1; // 标记为已关闭,避免重复关闭
}
}
1.2.6 辅助函数(网络字节序转换)
Socket 通信要求端口/IP 必须是网络字节序(大端序),而主机字节序可能是小端(x86 架构),因此需要转换:
| 函数 | 功能 | 适用场景 |
|---|---|---|
htons() |
主机字节序 → 网络字节序 | 端口号转换(16 位) |
ntohs() |
网络字节序 → 主机字节序 | 解析收到的端口号 |
htonl() |
主机字节序 → 网络字节序 | IP 地址转换(32 位) |
ntohl() |
网络字节序 → 主机字节序 | 解析收到的 IP 地址 |
inet_pton() |
点分十进制 → 网络字节序 | 字符串 IP 转二进制 |
inet_ntop() |
网络字节序 → 点分十进制 | 二进制 IP 转字符串 |
示例
cpp
// 端口转换:主机 8080 → 网络字节序
uint16_t port = htons(8080);
// IP 转换:字符串 "127.0.0.1" → 网络字节序
struct in_addr addr;
inet_pton(AF_INET, "127.0.0.1", &addr);
// 反向转换:网络字节序 IP → 字符串
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr, ip, sizeof(ip)); // ip = "127.0.0.1"
1.3 demo v1
1.3.1 源码
Server.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
using namespace LogModule;
const int defaultfd = -1;
class UdpServer
{
public:
UdpServer(const std::string &ip, uint16_t port)
: _sockfd(defaultfd),
_ip(ip),
_port(port)
{
}
void Init()
{
// 1. 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel ::FATAL) << "socket error !";
exit(1);
}
LOG(LogLevel::INFO) << "socket success, sockfd: " << _sockfd;
// 2. 绑定 socket信息,主要是ip和端口号
// 2.1 先填充结构体,供bind使用
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 先清零
local.sin_family = AF_INET; // 填协议家族
local.sin_port = htons(_port); // 填端口号,需要将本地主机序列转化为网络序列
// 填IP,需要先把IP字符串转成四字节格式(32位无符号整数),
// 然后四字节再转成网络序列,还好库已经提供实现转化的接口
local.sin_addr.s_addr = inet_addr(_ip.c_str());
// 2.2 绑定,注意第二个参数,为了更通用所以传基类,所以强转
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
// 2.3 判断是否绑定成功
if (n < 0)
{
LOG(LogLevel ::FATAL) << "bind error!";
exit(2);
}
// 绑定成功
LOG(LogLevel ::INFO) << "bind success, sockfd: " << _sockfd;
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
// 1. 读取,收消息
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
size_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (s > 0)
{
buffer[s] = 0;
LOG(LogLevel ::DEBUG) << "buffer: " << buffer;
// 2. 响应,发消息
std::string echo_string = "server echo@ ";
echo_string += buffer;
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
}
}
}
~UdpServer()
{
}
private:
int _sockfd;
uint16_t _port;
std::string _ip; // 本地服务器用的是点分十进制风格,字符串
bool _isrunning;
};
Client.cc
cpp
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
// 使用格式: ./upclient server_ip server_port
int main(int argc, char *argv[])
{
// 预备工作
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// 访问服务器
// 1. 创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std ::endl;
return 2;
}
// 2. 2.1绑定。意义是将本地的ip,port和上面的文件关联。
// client不需要显示绑定,首次发送消息,OS会自动给client进行绑定,
// 操作系统知道客户端IP,端口号随机分配 。
// 不显示绑定的原因:为了避免client端口冲突。
// client的端口号是几不重要,只要是唯一的就行。
// 2.2 填写服务器信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
while (true)
{
std::string input;
std::cout << "Please inpute: ";
std::getline(std::cin, input);
int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
(void)n;
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
Server.cc
cpp
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
// 使用格式 ./udpserver ip port
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " ip port" << std::endl;
return 1;
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
Enable_Console_Log_Strategy();
std::unique_ptr<UdpServer> user = std::make_unique<UdpServer>(ip, port);
user->Init();
user->Start();
return 0;
}
1.3.2 实验结果

1.3.3 流程详解

先运行UdpServer.cc,运行格式必须为
./udpserver ip port
进行参数判断,第一个参数是程序名字,
第二个参数是ip地址,
第三个参数是端口号。
如果不是该格式运行,提示运行错误。
往下,创建ip字符串(因为是点分十进制存储,所以是字符串类型),赋值参数
创建port,看到类型是uint16_t,我们转到定义查看


发现实际上就是一个短整型。赋值要转为int

然后,使用智能指针创建服务器对象,user。
我们转到UdpServer.hpp

先看一下成员变量,分别是_sockfd(套接字文件描述符)。
有socket套接字的文件描述符,服务端的IP和端口,还有服务端是否运行的标志位。
构造函数

有默认的_sockfd,需要输入ip 和 端口号。

创建user对象的时候已经输入参数,这个参数是调用程序的时候传的

至此对象创建完毕,调用对象的init方法。

首先创建套接字,指定通信方式,说明通信类型,自动选择协议。

创建成功返回文件描述符。失败打印错误信息。

下一步:绑定socket信息,绑定的目的是:
Socket 本身是操作系统内核里的一个 "通信端点" 结构体,bind()是你(应用程序)告诉操作系统:
"请把「本机 IP + 端口」分配给这个 Socket,以后所有发往这个 IP + 端口的数据包,都交给我这个进程处理;我从这个 Socket 发出去的数据包,也用这个 IP + 端口作为源地址。"
简单说:bind()是应用程序向操作系统 "认领" IP + 端口,而非 socket "主动知道"。
socket相当于一个窗口,或者文件指针:而这个文件被一个特定进程写的,所以要绑定IP和port
在绑定之前要先填写结构体
填写结构体之前先清零,用的bzero函数。然后填写家族协议,IP,端口号。
对于端口号,需要将本地主机序列转化为网络序列
填IP,需要先把IP字符串转成四字节格式(32位无符号整数),然后四字节再转成网络序列,还好库已经提供实现转化的接口

填写完结构体,进行绑定,提供参数,进程绑定的套接字(窗口/文件),服务端的信息(IP, port),信息长度。
最后检查是否绑定成功。

随后调用Start函数。运行服务端(是一个死循环)。

使用recvfrom函数接受来自指定socket窗口的数据。
填写参数:文件描述符(指明要从哪个窗口(文件)拿数据),buffer(拿到数据之后放在哪里),第三个参数(缓冲区大小),第四个参数:接受数据的方式(阻塞?非阻塞?),第五个参数:类型sockaddr_in实际要强转成sockaddr,是一个输出型参数,用于带回发送者的信息(IP,port),第六个参数是结构体的大小
recvfrom的peer是输出型参数,用来记录 "数据是谁发的"(服务器的 IP + 端口)
!!!学到这里完全明白套接字的意义了,看作是窗口就太棒了!
关于sockaddr_in

实际上就是

ok,已经从网卡中拿到数据传给了对应的socket窗口,又通过socket传给了对应的进程。

现在要实现服务器给客户端回显。
判断是否接受数据成功,将缓冲区的最后一个数据的下一个数据赋值0。
拼接字符串,目的是格式化输出。
最后使用sendto函数,发送给对应的主机进程。
参数是:要发的socket窗口,要发的内容,发送内容的大小,发送的对象信息(通过recvfrom带回来的)
Q:recvfrom为什么能带回来?为啥他就知道要从网卡中拿哪个数据?
A:因为有sockfd啊,已经给指明窗口了!!这里设计确实妙。
ok,这里服务端就设计完了。
下面看用户端:

首先指定客户端进程调用的格式,必须传ip和对应端口号。
创建套接字


先填写服务器结构体信息,这里使用memset清零。填进来服务器 对应的IP和port。
这里不用和socket进行显示绑定。
原因:client不需要显示绑定。首次客户端向服务端发送消息时,OS会自动给client进行绑定,操作系统知道客户端IP,对于端口号要采用随机分配 。
不显示绑定的原因:为了避免client端口冲突。
那为什么服务器要绑定?
核心原因
- 服务端:是「被动被访问方」,必须绑定固定端口作为"唯一寻址标识",让所有客户端能精准定位到该进程;若不绑定,客户端无固定目标,无法建立通信。
- 客户端:是「主动访问方」,发起通信时操作系统会自动分配临时端口,服务端可通过数据包中的"客户端源端口"回复数据,无需固定端口。
嗷!懂了,对于服务器来说,客户端的信息本来就是知道的,因为是客户端主动发起的通信。
emmm。。。咋能问出这问题,服务端不绑定的话客户端就不知道访问谁了呀。
完事儿,向服务器发送信息:

同样是一个死循环,从cin流中获取数据放在input中。
调用sendto函数,将信息发送给特定服务器。
然后再从对应socket窗口获取数据。再多说一下peer参数,他是一个输出型参数,在这里就存放的是服务器的信息,放在这里其实没啥用,起一个占位的作用,因为我们客户端已经知道服务器信息了。
终于弄懂了...
1.4 demo v2
优化代码,首先搞一个问题
Q:服务器需不需要手动绑定IP?
A:"服务器端不建议手动绑定特定IP"
核心原因是限制了服务器的访问范围,降低了灵活性和兼容性 ------手动绑一个具体IP(比如192.168.1.100),会导致服务器只能接收发往这个IP的请求,而绑定INADDR_ANY(0.0.0.0)能接收本机所有网卡的请求,这是服务器的最佳实践。
- 多网卡场景兼容 :服务器可能有多个网卡(有线/无线/内网/外网IP),绑特定IP只会响应该网卡的请求,其他网卡的请求会被丢弃;绑
INADDR_ANY能接收所有网卡的请求。 - IP变动适配 :服务器IP可能因网络配置(如DHCP、云服务器弹性IP)变化,绑特定IP会导致IP变了之后服务不可用,
INADDR_ANY无需修改代码。 - 部署简化:无需提前知道服务器的具体IP,代码可直接跨环境部署(开发/测试/生产),不用因IP不同改配置。
嗷 懂了 服务器可能有多个IP 如果绑特定的 就会丢弃其他IP收到的信息
完整代码:
sever.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
using namespace LogModule;
const int defaultfd = -1;
class UdpServer
{
public:
UdpServer(uint16_t port)
: _sockfd(defaultfd),
_port(port)
{
}
void Init()
{
// 1. 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel ::FATAL) << "socket error !";
exit(1);
}
LOG(LogLevel::INFO) << "socket success, sockfd: " << _sockfd;
// 2. 绑定 socket信息,主要是ip和端口号
// 2.1 先填充结构体,供bind使用
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 先清零
local.sin_family = AF_INET; // 填协议家族
local.sin_port = htons(_port); // 填端口号,需要将本地主机序列转化为网络序列
// 填IP,需要先把IP字符串转成四字节格式(32位无符号整数),
// 然后四字节再转成网络序列,还好库已经提供实现转化的接口
// local.sin_addr.s_addr = inet_addr(_ip.c_str());
local.sin_addr.s_addr = INADDR_ANY;
// 2.2 绑定,注意第二个参数,为了更通用所以传基类,所以强转
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
// 2.3 判断是否绑定成功
if (n < 0)
{
LOG(LogLevel ::FATAL) << "bind error!";
exit(2);
}
// 绑定成功
LOG(LogLevel ::INFO) << "bind success, sockfd: " << _sockfd;
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
// 1. 读取,收消息
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
size_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (s > 0)
{
int peer_port = ntohs(peer.sin_port);
std::string peer_ip = inet_ntoa(peer.sin_addr);
buffer[s] = 0;
LOG(LogLevel ::DEBUG) << "[" << peer_ip << ":" << peer_port << "]# " << buffer;
// 2. 响应,发消息
std::string echo_string = "server echo@ ";
echo_string += buffer;
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
}
}
}
~UdpServer()
{
}
private:
int _sockfd;
uint16_t _port;
// std::string _ip; // 本地服务器用的是点分十进制风格,字符串
bool _isrunning;
};
server.cc
cpp
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
// 使用格式 ./udpserver ip port
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return 1;
}
// std::string ip = argv[1];
uint16_t port = std::stoi(argv[1]);
Enable_Console_Log_Strategy();
std::unique_ptr<UdpServer> user = std::make_unique<UdpServer>(port);
user->Init();
user->Start();
return 0;
}
client.cc
cpp
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
// 使用格式: ./upclient server_ip server_port
int main(int argc, char *argv[])
{
// 预备工作
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// 访问服务器
// 1. 创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std ::endl;
return 2;
}
// 2. 2.1绑定。意义是将本地的ip,port和上面的文件关联。
// client不需要显示绑定,首次发送消息,OS会自动给client进行绑定,
// 操作系统知道客户端IP,端口号随机分配 。
// 不显示绑定的原因:为了避免client端口冲突。
// client的端口号是几不重要,只要是唯一的就行。
// 2.2 填写服务器信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
while (true)
{
std::string input;
std::cout << "Please inpute: ";
std::getline(std::cin, input);
int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
(void)n;
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
2. DictServer
2.1 功能介绍
实现一个简单的英译汉的网络字典
2.2 源码
dict.hpp
cpp
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "Log.hpp"
#include "InetAddr.hpp"
const std::string defaultdict = "./dictionary.txt";
const std::string sep = ": ";
using namespace LogModule;
class Dict
{
public:
Dict(const std::string &path = defaultdict) : _dict_path(path)
{
}
bool LoadDict()
{
std::ifstream in(_dict_path);
if (!in.is_open())
{
LOG(LogLevel::DEBUG) << "打开字典: " << _dict_path << " 错误";
return false;
}
std::string line;
while (std::getline(in, line))
{
auto pos = line.find(sep);
if (pos == std::string::npos)
{
LOG(LogLevel::WARNING) << "解析: " << line << " 失败";
continue;
}
std::string english = line.substr(0, pos);
std::string chinese = line.substr(pos + sep.size());
if (english.empty() || chinese.empty())
{
LOG(LogLevel::WARNING) << "没有有效内容: " << line;
continue;
}
_dict.insert(std::make_pair(english, chinese));
LOG(LogLevel::DEBUG) << "加载: " << line;
}
in.close();
return true;
}
std::string Translate(const std::string &word, InetAddr &client)
{
auto iter = _dict.find(word);
if (iter == _dict.end())
{
LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip() << " : " << client.Port() << "]# " << word << "->None";
return "None";
}
LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip() << " : " << client.Port() << "]# " << word << "->" << iter->second;
return iter->second;
}
~Dict()
{
}
private:
std::string _dict_path;
std::unordered_map<std::string, std::string> _dict;
};
server.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"
#include "InetAddr.hpp"
using func_t = std::function<std::string(const std::string&, InetAddr&)>;
using namespace LogModule;
const int defaultfd = -1;
class UdpServer
{
public:
UdpServer(uint16_t port, func_t func)
: _sockfd(defaultfd),
_port(port),
_isrunning(false),
_func(func)
{
}
void Init()
{
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel ::FATAL) << "socket error !";
exit(1);
}
LOG(LogLevel::INFO) << "socket 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;
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel ::FATAL) << "bind error!";
exit(2);
}
LOG(LogLevel ::INFO) << "bind success, sockfd: " << _sockfd;
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
size_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (s > 0)
{
InetAddr client(peer);
buffer[s] = 0;
std::string result = _func(buffer, client);
sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);
}
}
}
~UdpServer()
{
}
private:
int _sockfd;
uint16_t _port;
bool _isrunning;
func_t _func;
};
server.cc
cpp
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
#include "Dict.hpp"
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return 1;
}
uint16_t port = std::stoi(argv[1]);
Enable_Console_Log_Strategy();
Dict dict;
dict.LoadDict();
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&dict](const std::string &word, InetAddr &cli) -> std::string
{ return dict.Translate(word, cli); });
usvr->Init();
usvr->Start();
return 0;
}
client.cc
cpp
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std ::endl;
return 2;
}
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
while (true)
{
std::string input;
std::cout << "Please inpute: ";
std::getline(std::cin, input);
int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
(void)n;
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
2.3 实验结果

3. 简单聊天室
3.1 源码
完