文章目录
- 1.UDP建立
- 2.服务端
-
- [2.1 启动函数main](#2.1 启动函数main)
- [2.2 服务端创建](#2.2 服务端创建)
-
- [2.2.1 创建套接字](#2.2.1 创建套接字)
- [2.2.2 绑定端口号和IP地址](#2.2.2 绑定端口号和IP地址)
- [2.2.3 启动服务端收发功能](#2.2.3 启动服务端收发功能)
- 3.客户端
- 4.运行效果
- 5.常用网络命令
-
- [5.1 ping](#5.1 ping)
- [5.2 netstat](#5.2 netstat)
- 希望读者们多多三连支持
- 小编会继续更新
- 你们的鼓励就是我前进的动力!
1.UDP建立
由于
UDP「无连接 、不可靠 、面向数据报 」的特性,分服务端 和客户端 两步实现,核心是「套接字创建 + 数据收发」
2.服务端
2.1 启动函数main
main.cpp:
cpp
#include "UdpServer.hpp"
Log logger;
void Usage(const char *proc)
{
std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = std::stoi(argv[1]);
// 设置日志输出到屏幕
logger.Enable(Screen);
std::unique_ptr<UdpServer> svr(new UdpServer(port));
svr->Init();
svr->Run();
return 0;
}
设置命令行参数,启动服务端的命令必须为两个,Usage 函数用于提示用户正确输入,一个是可执行文件 ./UdpServer,另一个是指定执行的端口(注意要使用 1024+ 的,因为 0~1024 的端口通常包含具体对应的端口,容易产生冲突),为什么不用设置 IP 地址呢?下面的服务端地址会解释,然后使用 unique_ptr 智能指针创建唯一的服务端对象,避免多端启动,同时能够自动回收资源,最后依次 Init 初始化,Run 启动
2.2 服务端创建
cpp
#pragma once
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <memory>
#include <string>
#include <errno.h>
#include "log.hpp"
extern Log logger;
enum
{
SOCKET_ERROR = 1,
BIND_ERROR = 2
};
uint16_t default_port = 8080;
std::string default_ip = "0.0.0.0";
const int size = 1024;
class UdpServer
{
public:
UdpServer(uint16_t port = default_port, const std::string &ip = default_ip)
: port_(port)
, ip_(ip)
, sockfd_(0)
{}
void Init()
{
// 1.创建套接字
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd_ < 0)
{
logger(Fatal, "socket create error, sockfd: %d", sockfd_);
exit(SOCKET_ERROR);
}
logger(Info, "socket create success, sockfd: %d", sockfd_);
// 2.绑定端口号和IP地址
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port_); // 转换为网络字节序
// local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 转换为网络字节序
local.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定本机所有IP地址
int n = bind(sockfd_, (struct sockaddr*)&local, sizeof(local));
if(n < 0)
{
logger(Fatal, "socket bind error, errno: %d, err string: %s", errno, strerror(errno));
exit(BIND_ERROR);
}
logger(Info, "socket bind success, port: %d, ip: %s", port_, ip_.c_str());
}
void Run()
{
isrunning_ = true;
char inbuffer[size];
while(true)
{
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)
{
logger(Warning, "recvfrom client data error, n: %ld, errno: %d, err string: %s", n, errno, strerror(errno));
continue;
}
inbuffer[n] = 0;
std::string info = inbuffer;
std::string echo_string = "server echo# " + info;
std::cout << echo_string << std::endl;
sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len);
}
}
~UdpServer()
{
if(sockfd_ > 0)
{
close(sockfd_);
}
}
private:
int sockfd_;
uint16_t port_;
std::string ip_;
bool isrunning_;
};
UdpServer 构造函数初始化文件描述符,端口和 IP 地址,创建套接字的过程其实就是创建一个文件,对于服务器来说,你所看到的 IP 其实是处理过的,不是实际 IP,云服务器禁止直接绑定公网 IP,但是内网 IP 可以
通常服务端绑定 0.0.0.0 表示接收所有客户端的请求,若指定具体 IP(如 192.168.1.100),则只有发往该 IP 的数据能被接收,其他 IP 会被丢弃;绑定 0.0.0.0 则兼容所有网卡,客户端无论从本机、内网、外网访问,都能连接到服务端,无需关心主机的具体 IP 配置
2.2.1 创建套接字
cpp
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd_ < 0)
{
logger(Fatal, "socket create error, sockfd: %d", sockfd_);
exit(SOCKET_ERROR);
}
logger(Info, "socket create success, sockfd: %d", sockfd_);

domain 表示协议族,最常见的就是 AF_INET 和 AF_INET6,表示 IPV4 和 IPV6 网络协议。type 表示套接字类型,决定了数据传输的方式,SOCK_STREAM 表示字节流,通常指 TCP;SOCK_DGRAM 表示数据流,通常指 UDP;protocol 表示具体协议,通常设为 0,让系统根据 domain 和 type 自动选择默认协议


2.2.2 绑定端口号和IP地址
如果把 socket() 比作买了一部电话机,那么这段代码就是在去电信局申请电话号码(端口)并把电话线插到墙上的电话口(网卡)上,准备开始接听电话。
cpp
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()); // 转换为网络字节序
local.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定本机所有IP地址
int n = bind(sockfd_, (struct sockaddr*)&local, sizeof(local));
if(n < 0)
{
logger(Fatal, "socket bind error, errno: %d, err string: %s", errno, strerror(errno));
exit(BIND_ERROR);
}
logger(Info, "socket bind success, port: %d, ip: %s", port_, ip_.c_str());
struct sockaddr_in 结构体用于存储需要绑定的信息,告诉套接字应该监听本机的哪个 IP 和哪个端口

转到定义就发现这个宏其实就是 sin_family,## 是 C 语言里的一种拼接用法

sin_port 本质上是 16 比特位类型

sin_addr 本质是 32 比特位类型

首先对创建的结构体 local,使用 bzero 对结构体初始化清零,然后依次设置 sin_family、sin_port、sin_addr,注意端口和 IP 都需要转为网络序列,因为网络设备无法确定他的大小端优先级,最后进行 bind 绑定

h = host(主机,即你的电脑)n = network(网络)to =转换方向s = short(16位整数,通常用于端口号)l = long(32位整数,通常用于IPv4地址)
| 功能 | 老式函数 (仅IPv4) | 现代推荐函数 (支持IPv4 & IPv6) | 优势 |
|---|---|---|---|
| IP字符串 转 二进制 | inet_addr / inet_aton |
inet_pton |
支持 IPv6,返回值处理更清晰 |
| 二进制 转 IP字符串 | inet_ntoa |
inet_ntop |
线程安全(需要用户自己提供 buffer),支持 IPv6 |
p = presentation(表达格式,即字符串)n = numeric(数值格式,即二进制)pton = p to n(字符串转数字)ntop = n to p(数字转字符串)
2.2.3 启动服务端收发功能
cpp
isrunning_ = true;
char inbuffer[size];
while(true)
{
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)
{
logger(Warning, "recvfrom client data error, n: %ld, errno: %d, err string: %s", n, errno, strerror(errno));
continue;
}
inbuffer[n] = 0;
std::string info = inbuffer;
std::string echo_string_c = "client echo# " + info + " [这是客户端发送的信息]";
std::cout << echo_string_c << std::endl;
std::string echo_string_s = "server echo# " + info + " [这是服务器端发送的信息]";
sendto(sockfd_, echo_string_s.c_str(), echo_string_s.size(), 0, (struct sockaddr*)&client, len);
std::cout << echo_string_s << std::endl;

sockfd:套接字描述符(Socket)。buf:要发送的数据缓冲区指针(信的内容)。len:要发送的数据长度(信的字数)。flags:通常设为 0。dest_addr:(关键) 目标地址结构体指针。包含接收方的 IP 和端口。addrlen:目标地址结构体的长度(sizeof(struct sockaddr_in))。

sockfd:套接字描述符。buf:接收数据的缓冲区(准备好的空信纸)。len:缓冲区的大小(防止溢出)。flags:通常设为 0。src_addr:(关键,输出参数) 这是一个空指针,函数返回时,系统会把发送方 的 IP 和端口填在这个结构体里。如果你不关心谁发的,可以填NULL。addrlen:(关键,输入输出参数)
3.客户端
cpp
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <string>
using namespace std;
void Usage(const char *proc)
{
std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(0);
}
string serverip = argv[1];
uint16_t serverport = stoi(argv[2]);
// 1.创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cerr << "socket create error, sockfd: " << sockfd << std::endl;
exit(1);
}
std::cout << "socket create success, sockfd: " << sockfd << std::endl;
// 2.准备服务器地址信息
struct sockaddr_in serveraddr;
bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(serverport); // 转换为网络字节序
serveraddr.sin_addr.s_addr = inet_addr(serverip.c_str()); // 转换为网络字节序
// 3.发送数据
char buffer[1024];
while(true)
{
cout << "Please Enter@ ";
cin.getline(buffer, sizeof(buffer));
sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
// 接收数据
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&tmp, &len);
if(n > 0)
{
buffer[n] = 0;
cout << buffer << endl;
}
}
// 4.关闭套接字
close(sockfd);
return 0;
}
客户端的代码和服务端大致相同,不同地方在于客户端需要指定的是自己要链接哪个端口和 IP 地址,该IP地址是公网IP,端口是指定服务端的端口
4.运行效果

5.常用网络命令
5.1 ping

ping 命令是用于测试两台设备之间网络连通性、检测延迟和丢包率的网络诊断工具
5.2 netstat

netstat 是一款用于查看本机网络连接状态、路由表、网络接口信息的命令行工具,可帮助排查网络连接问题、监控端口占用情况
| 参数 | 作用 | 备注 |
|---|---|---|
-a |
显示所有网络连接(包括监听、已建立、关闭等待等状态) | 最基础的全量查看参数 |
-n |
以数字形式显示 IP 和端口(不解析域名、不转换端口名称) | 提升执行速度,避免 DNS 解析延迟 |
-p |
显示进程信息(进程 ID + 进程名称) | 需要 root 权限 (sudo)才能查看所有用户的进程 |
-t |
仅筛选 TCP 协议的连接 | 聚焦 TCP 相关的监听/连接状态 |
-u |
仅筛选 UDP 协议的连接 | UDP 无连接状态,仅显示监听端口 |
-l |
仅显示监听状态的端口 | 排查服务是否正常监听(如 Web 服务 80 端口) |
-r |
显示内核路由表 | 等同于 route -n 命令 |
-i |
显示网络接口的流量统计(收发字节数、丢包数等) | 排查网卡是否有异常丢包 |
希望读者们多多三连支持
小编会继续更新
你们的鼓励就是我前进的动力!
