在上篇文章 【Linux】: Socket 编程 里面已经关于 socket 网络编程的前置知识,这里我们就来实际运用一下其 套接字 来实现相关的套接字编程吧
老样子,先写 Makefile 文件,如下:
.PHONY:all
all:server_udp client_udp
server_udp:UdpServerMain.cc
g++ -o $@ $^ -std=c++17 -lpthread
client_udp:UdpClientMain.cc
g++ -o $@ $^ -std=c++17 -lpthread
.PHONY:clean
clean:
rm -f server_udp client_udp
先写出我们要实现的大概框架,将之前我们实现的 日志封装 Log.hpp【Linux】:日志策略 + 线程池(单例模式) 以及 互斥量封装【【Linux】:多线程(互斥 && 同步)】 Mutex.hpp 文件复制粘贴到当前文件夹下,基本运行框架如下:
UdpServerMain.cc
#include "UdpServer.hpp"
int main()
{
ENABLE_CONSOLE_LOG();
std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>();
svr_uptr->InitServer();
svr_uptr->Start();
return 0;
}
然后再编写一个头文件,为了后面的编程方便和美观,就将其另开一个头文件
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
};
分析如下:
这里我们定义了基本的错误码、一个终止程序的宏 Die
和一个类型转换宏 CONV
,主要用于网络编程(套接字编程),通过宏来简化错误处理和类型转换的代码
#ifndef _UDP_SERVER_HPP__
#define _UDP_SERVER_HPP__
#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include "Log.hpp"
#include "Common.hpp"
using namespace LogModule;
const static int gsockfd = -1;
const static std::string gdefaultip = "127.0.0.1"; // 表示本地主机
// 这里的测试端口号用 8080,这里端口号可以随机,但是 1024 以内的不行,1024 以上包括 1024 是可以的,3306 也不大行,因为 3306 是 mySQL 端口号,尽量不要和系统端口号冲突,因此我们可以用 8080 8081 没有人用的端口号
const static uint16_t gdefaultport = 8080;
class UdpServer
{
public:
UdpServer(const std::string &ip = gdefaultip, uint16_t port = gdefaultport)
: _sockfd(gsockfd),
_ip(ip),
_port(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; // 测试
// 2. 填充网络信息,并且进行绑定
// 2.1 这里没有把 socket信息设置进入内核,只是填充了结构体
struct sockaddr_in local;
// 因为 sockaddr_in 开始里面有填充字段,因此我们要清0
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = ::htons(_port); // 要被发送给对方,既要发到网络中,把端口号由 主机序列 转成 网络序列(htons)
// 两步骤: 1. string ip -> 4 bytes 2. 4 bytes -> network order
// 这里直接赋值会报错,是因为 C 语言中结果体只能整体初始化,而不能被赋值,因此我们只需给 结构体 的某个特定数据赋值就行
local.sin_addr.s_addr = ::inet_addr(_ip.c_str());
// 左手文件信息(1) 右手网络信息(2),将两者结合得到 绑定信息
// 2.2 bind: 设置进入内核中
int n = ::bind(_sockfd, CONV(&local), sizeof(local));
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) ; // -1 预留一个空位置给 \0,这里 0 表示采用阻塞的方式进行等待
if(n > 0)
{
// 1. 消息内容 && 2. who 发 me 的
inbuffer[n] = 0;
LOG(LogLevel::DEBUG) << "client say@" << inbuffer;
std::string echo_string = "echo# ";
echo_string += inbuffer;
::sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, CONV(&peer), sizeof(peer));
}
}
}
~UdpServer()
{}
private:
int _sockfd;
uint16_t _port; // 服务器未来的端口号 38m-9s
std::string _ip; // 服务器对应的 ip-TODO 地址
bool _isrunning; // 服务器运行状态
};
#endif
🥝
代码片段分析:
在将 主机 IP 和 主机端口号 转化为 网络序列 时候,我们需要了解一些相关函数,而且也会遇到一些问题,如下:
全双工模式:允许数据同时在两个方向上传输,要求发送和接收色好吧都有独立的接收和发送能力
🥝 InitServer()
方法::socket(AF_INET, SOCK_DGRAM, 0)
创建一个 UDP 套接字。sockaddr_in
结构体并调用 ::bind
将套接字绑定到指定的 IP 和端口。 local.sin_family = AF_INET
:设置协议族为 IPv4。local.sin_port = ::htons(_port)
:将端口号转换为网络字节序。local.sin_addr.s_addr = ::inet_addr(_ip.c_str())
:将 IP 地址字符串转换为网络字节序。::bind
将本地地址与套接字关联起来,CONV(&local)
将 local
结构体转换为 sockaddr*
类型。🥝 Start()
方法::recvfrom
接收来自客户端的数据。inbuffer
用于存储接收到的数据,peer
用于存储客户端的地址信息。::sendto
将回显字符串发送回客户端。此时我们的代码实现的差不多了,运行如下:
为了证明这个服务器是成功跑了起来,查看在 Linux 中为网络服务是否启动 -- netstat -uap
🔥 现在我们就可以知道我们的服务已经跑起来了,其实现在我们也可以知道 网络服务 就是一个进程,或者进程池,或者 一堆进程 / 进程池。
📕 现在我们就可以来实现我们的客户端了,服务器是个被动的,其必须得知道目标服务器的 IP 和 端口号,我们就可以通过 main(argc, argv[]) 来拿到,因此下面我们实现了一个简单的 UDP 客户端,能够向指定的服务器发送消息并接收回显
#include "UdpClient.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 "Common.hpp"
// 客户端 访问 目标服务器
// ./client_udp sereverip 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);
// client 需不需要 bind? socket <--> socket
// client 必须也要有 自己 的 ip 和 端口, 但是客户端不需要自己显示的调用 bind
// 而是客户端首次 sendto 消息 的时候, 由 OS 自动进行 bind
// 1.如何理解 client 自动随机 bind 端口号
// OS 不允许一个端口号绑定多个进程,必须要有唯一性,但是 一个进程可以绑定 多个端口号
// 保证当前端口号唯一性,和别人不冲突就行
// 2.如何理解 server 要显示 bind
// 服务器的端口号必须稳定
// 而且必须是众所周知而且不能轻易改变的
int n = ::sendto(sockfd, message.c_str(), message.size(), 0,CONV(&server), sizeof(server));
(void) n;
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
char buffer[64];
n = ::recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&tmp), &len);
if(n > 0)
{
buffer[n] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
1. 命令行参数检查
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]);
argv[1]
为服务器的 IP 地址,argv[2]
为服务器的端口号。2. 创建 UDP 套接字
int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cerr << "socket error" << std::endl;
Die(SOCKET_ERR);
}
AF_INET
(IPv4)、SOCK_DGRAM
(UDP)协议。sockfd
小于 0),则输出错误信息并终止程序。3. 填充服务器信息
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());
sockaddr_in
结构体,用来指定目标服务器的地址和端口信息。 server.sin_family = AF_INET;
设置为 IPv4 地址族。server.sin_port = htons(serverport);
将端口号转换为网络字节序(大端序)。server.sin_addr.s_addr = ::inet_addr(serverip.c_str());
将服务器的 IP 地址转换为网络字节序的 in_addr
。4. 发送消息到服务器
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;
::sendto
发送到服务器。 sockfd
为套接字文件描述符。message.c_str()
获取字符串的 C 风格字符指针。sizeof(server)
表示服务器地址的大小。sendto
函数会将数据包发送到目标服务器。在这里,客户端并没有显式绑定本地端口,操作系统会自动为客户端分配一个临时的端口进行发送。5. 接收服务器的回显
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
char buffer[64];
n = ::recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&tmp), &len);
if(n > 0)
{
buffer[n] = 0;
std::cout << buffer << std::endl;
}
recvfrom
接收来自服务器的回显数据。 buffer
存储接收到的消息。tmp
用于存储服务器地址信息,len
用于获取地址的大小。recvfrom
会阻塞,直到从服务器接收到数据。客户端自动绑定端口
bind
,操作系统会在首次调用 sendto
时为客户端自动分配一个临时端口。操作系统会确保每个进程的端口号是唯一的,并避免端口冲突。服务器端口和客户端端口的区别
结果如下:
通过 netstat -nuap 指令查询如下:
我们这不仅仅打印相关消息,而且也要知道是谁发的信息,因此我们需要 去修改 UdpServer.hpp 文件,如下:
打印结果如下:
我们这应该要进行网络通信啊,而不是仅仅局限于本地通信
#include "UdpServer.hpp"
// ./server_udp localip localport
int main(int argc, char *argv[])
{
if(argc != 3)
{
std::cerr << "Usage: " << argv[0] << " localIp localPort" << std::endl;
Die(USAGE_ERR);
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
ENABLE_CONSOLE_LOG();
std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(ip, port);
svr_uptr->InitServer();
svr_uptr->Start();
return 0;
}
按照指定 IP 来 运行,如下:
但是当我们要进行网络通信时候,把我们的地址换成我们的云服务器地址,显示的结果却和我们想象的不大一样,这是为什么呢?
🔥 比如一个机器上可能有多个 IP,对我们来说,这些 IP 都是发给服务器的,但是在我们的应用层,如果只绑定了一个 IP,那么只能获得这台主机上特定某个 IP 的信息, 但是在将来用户可能通过各种不同的IP来发送给相同的端口号,此时主机也只能获得已经 绑定 IP 的信息。
因此我们这里代码书写还是有问题的,在正常的服务器其实是不用 IP 的,只需要端口,在填充的时候,也就需要做出相应修改
结果运行如下:
使用 netstat -nuap 查询显示如下:
将我们运行生成的 client_udp 文件打包到当前主机的桌面上,然后用另外一个 Linux 用户进行打开解包安装如下:
由于此时的文件默认没有可执行,因此我们还需要给其加上权限
验证如下:
现在我们已经能实现 udp 编程了,然后我们再对我们的代码进行调整
#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();
}
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 GetIp() { return _ip; }
uint16_t GetPort() { return _port; }
~InetAddr()
{}
private:
struct sockaddr_in _net_addr;
std::string _ip;
uint16_t _port;
};
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 LogMudule;
const static int gsockfd = -1;
// 这里的测试端口号用 8080,这里端口号可以随机,但是 1024 以内的不行,1024 以上包括 1024 是可以的,3306 也不大行,因为 3306 是 mySQL 端口号,尽量不要和系统端口号冲突,因此我们可以用 8080 8081 没有人用的端口号
const static uint16_t gdefaultport = 8080;
class UdpServer
{
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; // 测试
// 信息填充字段都可以删去了
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) ; // -1 预留一个空位置给 \0,这里 0 表示采用阻塞的方式进行等待
if(n > 0)
{
// 1. 消息内容 && 2. who 发 me 的
InetAddr cli(peer);
inbuffer[n] = 0;
std::string clientinfo = cli.GetIp() + ":" + std::to_string(cli.GetPort()) + " # " + 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; // 服务器运行状态
};
#endif
运行结果如下:
🔥 我们这里先中途插入一个趣味的翻译显示实验,在 EchoServer 的基础上来实现,大部分代码基本都没变,修改了一少部分代码,大家可以仔细看看
先给定一些等会我们要翻译的单词数据 dict.txt
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
IsLand1314: 本人
Common.hpp 修改如下:
Dictionary.hpp
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include <fstream>
#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)) // operator bool
{
// happy: 开心的
std::string key, 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;
};
UdpServer.hpp 修改如下:
#ifndef _UDP_SERVER_HPP__
#define _UDP_SERVER_HPP__
#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cerrno>
#include <strings.h>
#include <functional>
#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 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; // 测试
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) ; // -1 预留一个空位置给 \0,这里 0 表示采用阻塞的方式进行等待
if(n > 0)
{
// 把英文单词转化成为中文
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;
bool _isrunning; // 服务器运行状态
// 业务(回调方法)
func_t _func;
};
#endif
UdpServerMain.cc 修改如下:
#include "UdpServer.hpp"
#include "Dictionary.hpp"
// ./server_udp localip localport
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " localPort" << std::endl;
Die(USAGE_ERR);
}
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);
// func_t f = std::bind(&Dictionary::Translate, dict_sptr.get(), std::placeholders::_1); //std::placeholders::_1 是 C++11 引入的一个占位符,常用于绑定函数参数的操作,特别是在与 std::bind 配合使用
// std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(f, port);
svr_uptr->InitServer();
svr_uptr->Start();
return 0;
}
测试如下:
🔥 话说我们之前 Echoserver 已经实现了给我发信息,信息也已经可以返回给我的功能,但是如果同时有多个人要发信息的话,这个时候发去的信息就需要记录下来发来的人信息,并且进行维护,然后再把维护的信息给多个人一起看,这就实现了 群聊 的功能
在之前的代码当中,Echo 服务器收到发的信息,然后再转发对应的信息,但是有个问题,这里不仅要一个人收消息,后面还要我们自己去转发给所有人,此时收消息转消息都是同一个人,UDP 数据一旦过大,服务器可能就没时间接收数据了,而且我们前面也说过 UDP 套接字本身是全双工的,全双工的意思就是 在收数据的同时,也可以发送数据,下面我们举个例子
如果我们今天收到一个消息,并且将其封装成一个转发的任务,然后由其他线程来做转发, 而本身服务器只负责进行网络读
注意:我们这个代码是基于 EchoServer 基础上进行修改完善的
User.hpp
#pragma once
#include <iostream>
#include <string>
#include <list>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include "InetAddr.hpp"
#include "Log.hpp"
using namespace LogModule;
class UserInterface
{
public:
virtual ~UserInterface() = default;
virtual void SendTo(int sockfd, const std::string &message) = 0;
virtual bool operator ==(const InetAddr &u) = 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;
}
~User()
{}
private:
InetAddr _id;
};
// 对用户消息进行路由
// UserManage 把所有用户先管理起来
// 把一个新用户添加到在线用户列表,一旦要发信息,我们的 UserManage 不做发送,他要调的就是 User 提供的公共方法
// 这种设计模式就称为 观察者模式 -> observer
class UserManage
{
public:
UserManage()
{
}
void AddUser(InetAddr &id)
{
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)); // 构建 User 对象
}
void DelUser(const InetAddr &id)
{
}
void Router(int sockfd, const std::string &message) // 消息转发
{
for(auto &user : _online_user)
{
user->SendTo(sockfd, message);
}
}
~UserManage()
{}
private:
std::list<std::shared_ptr<UserInterface>> _online_user; // 在线用户
};
上面这段代码实现了一个简化的用户管理和消息转发系统。它使用了 C++ 的面向对象编程、智能指针以及观察者设计模式。以下是对代码的逐步分析:
1. UserInterface 类
class UserInterface
{
public:
virtual ~UserInterface() = default;
virtual void SendTo(int sockfd, const std::string &message) = 0;
virtual bool operator ==(const InetAddr &u) = 0;
};
UserInterface
是一个抽象基类,定义了用户类应实现的接口。
SendTo
方法用于向指定的套接字发送消息(纯虚函数,子类需要实现)。operator ==
用于比较用户的网络地址(也是纯虚函数,子类需要实现)。2. User 类
class UserInterface
{
public:
virtual ~UserInterface() = default;
virtual void SendTo(int sockfd, const std::string &message) = 0;
virtual bool operator ==(const InetAddr &u) = 0;
};
User
类继承自 UserInterface
,实现了 SendTo
和 operator ==
方法。InetAddr
来初始化 _id
。SendTo
方法通过 sendto
函数将消息发送到指定的用户。日志记录了发送的信息和目标地址。operator ==
用于比较两个 User
是否相同,依据是它们的 InetAddr
。User
对象。3. UserManage 类
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;
}
~User()
{}
private:
InetAddr _id; // 用户的网络地址
};
UserManage
类负责管理在线用户,主要功能包括: AddUser
方法会检查用户是否已经存在,若不存在则将用户加入到 _online_user
列表中。用户通过 InetAddr
标识。DelUser
方法目前是空的,可能在后续实现中用于移除用户。Router
方法会遍历所有在线用户,并调用每个用户的 SendTo
方法,将消息发送给所有用户。可以理解为消息的广播。_online_user
是一个 shared_ptr
类型的列表,管理所有在线用户,避免手动内存管理的麻烦。4. 观察者模式
代码采用了观察者模式(Observer Pattern),其中:
UserManage
是观察者(Observer),负责管理所有的用户,并能对用户的状态进行操作。User
是被观察者(Subject),通过 SendTo
方法接收并处理来自 UserManage
的消息。UserManage
会通知所有用户调用 SendTo
方法,这样的设计能有效地将消息发送逻辑和用户管理逻辑解耦。观察者模式概念
🔥 观察者模式(发布订阅模式)是一种行为型设计模式,用于定义对象之间的一种一对多的依赖关系,使得一个对象状态发生变化时,所有依赖它的对象都会收到通知并自动更新。
其目的:将观察者和被观察者代码解耦,使得一个对象或者说事件的变更,让不同观察者可以有不同的处理,非常灵活,扩展性很强,是事件驱动编程的基础。
举个例子:就相当于我们刷抖音,关注了一些博主,那么当博主发视频的时候,它的视频通知就会发送到每个关注它的粉丝那
UdpServer.hpp 修改如下:
#ifndef _UDP_SERVER_HPP__
#define _UDP_SERVER_HPP__
#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cerrno>
#include <strings.h>
#include <functional>
#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 uint16_t gdefaultport = 8080;
using adduser_t = std::function<void (InetAddr &id)>;
class UdpServer
{
public:
UdpServer(adduser_t adduser, uint16_t port = gdefaultport)
: _sockfd(gsockfd),
_addr(port),
_isrunning(false),
_adduser(adduser)
{
}
// 都是套路
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; // 测试
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) ; // -1 预留一个空位置给 \0,这里 0 表示采用阻塞的方式进行等待
if(n > 0)
{
// 1. 消息内容 && 谁发的
InetAddr cli(peer);
inbuffer[n] = 0;
// 2. 新增用户
_adduser(cli);
std::string clientinfo = cli.GetIp() + ":" + std::to_string(cli.GetPort()) + " # " + inbuffer;
// LOG(LogLevel::DEBUG) << "client say@" << 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;
};
#endif
UdpServerMain.cc 修改如下:
#include "UdpServer.hpp"
#include "User.hpp"
// ./server_udp localip localport
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " localPort" << std::endl;
Die(USAGE_ERR);
}
uint16_t port = std::stoi(argv[1]);
ENABLE_CONSOLE_LOG();
// 用户管理模块
std::shared_ptr<UserManage> um = std::make_shared<UserManage>();
// 网络模块
std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>([&um](InetAddr &id){
um->AddUser(id);}, port);
svr_uptr->InitServer();
svr_uptr->Start();
return 0;
}
测试如下:
上面只是一点点小小的修改,现在我们要开始引入我们之前写的 线程池,详情可以参考 文章
【Linux】:日志策略 + 线程池(单例模式) 单例模式下的线程池代码
ThreadPool.hpp 修改如下:
UdpServer.hpp 修改如下:
#ifndef _UDP_SERVER_HPP__
#define _UDP_SERVER_HPP__
#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cerrno>
#include <strings.h>
#include <functional>
#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"
#include "ThreadPool.hpp"
using namespace LogModule;
using namespace ThreadPoolModule;
const static int gsockfd = -1;
const static uint16_t gdefaultport = 8080;
using adduser_t = std::function<void (InetAddr &id)>;
using remove_t = std::function<void(InetAddr &id)>;
using route_t = std::function<void(int sockfd, const std::string &message)>;
using task_t = std::function<void()>;
class UdpServer
{
public:
UdpServer(adduser_t adduser, uint16_t port = gdefaultport)
: _sockfd(gsockfd),
_addr(port),
_isrunning(false),
_adduser(adduser)
{
}
// 都是套路
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; // 测试
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)
{
_adduser = adduser;
_route = route;
}
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) ; // -1 预留一个空位置给 \0,这里 0 表示采用阻塞的方式进行等待
if(n > 0)
{
// 1. 消息内容 && 谁发的
InetAddr cli(peer);
inbuffer[n] = 0;
std::string message = inbuffer;
// 2. 新增用户
_adduser(cli);
// 3. 构建转发任务,推送给线程池,让线程池进行转发
task_t task = std::bind(UdpServer::_route, _sockfd, message); // route 是回调方法,不是成员方法 不用 this
ThreadPool<task_t>::getInstance()->Equeue(task);
// std::string clientinfo = cli.GetIp() + ":" + std::to_string(cli.GetPort()) + " # " + inbuffer;
// // LOG(LogLevel::DEBUG) << "client say@" << 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;
route_t _route;
};
#endif
UdpServerMain.cc 修改如下:
#include "UdpServer.hpp"
#include "User.hpp"
// ./server_udp localip localport
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " localPort" << std::endl;
Die(USAGE_ERR);
}
uint16_t port = std::stoi(argv[1]);
ENABLE_CONSOLE_LOG();
// 用户管理模块
std::shared_ptr<UserManage> um = std::make_shared<UserManage>();
// 网络模块
std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>([&um](InetAddr &id){
um->AddUser(id);}, port);
svr_uptr->InitServer();
svr_uptr->RegisterService([&um](InetAddr &id)
{um->AddUser(id);},
[&um](int sockfd, const std::string &message)
{um->Router(sockfd, message);});
svr_uptr->Start();
return 0;
}
UdpClientMain.cc 修改如下:
#include "UdpClient.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 "Common.hpp"
int sockfd = -1;
void *Recver(void *args)
{
while(true)
{
(void) args;
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
char buffer[64];
int n = ::recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&tmp), &len);
if(n > 0)
{
buffer[n] = 0;
std::cout << buffer << std::endl;
}
}
}
// 客户端 访问 目标服务器
// ./client_udp sereverip 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
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());
pthread_t tid;
pthread_create(&tid, nullptr, Recver, nullptr);
// 2. clientdone
while(true)
{
std::cout << "Please Enter# " << std::endl;
std::string message;
std::getline(std::cin, message);
// client 需不需要 bind? socket <--> socket
// client 必须也要有 自己 的 ip 和 端口, 但是客户端不需要自己显示的调用 bind
// 而是客户端首次 sendto 消息 的时候, 由 OS 自动进行 bind
// 1.如何理解 client 自动随机 bind 端口号
// OS 不允许一个端口号绑定多个进程,必须要有唯一性,但是 一个进程可以绑定 多个端口号
// 保证当前端口号唯一性,和别人不冲突就行
// 2.如何理解 server 要显示 bind
// 服务器的端口号必须稳定
// 而且必须是众所周知而且不能轻易改变的
int n = ::sendto(sockfd, message.c_str(), message.size(), 0,CONV(&server), sizeof(server));
(void) n;
}
return 0;
}
测试如下:
利用我们之前在
【Linux】IPC 进程间通信(一):管道(匿名管道&命名管道)讲的管道内容以及在 【Linux】: 重定向(补充)重定向内容,我们来演示一下其在管道上的表示
先对上面代码进行一下修改,方便输出信息的分离,UdpClientMain.cc 修改如下
演示如下:
聊天室--管道
🔥 这里在 User 文件进行操作时,Router 要遍历链表 ,而 Adduser 也要向链表里做插入,而我们的链表可能会被数据接收模块、线程池模块并发地访问它,因此我们这里转发的时候需要做一下修改,必须保证临界资源安全,修改如下:
但是,当我们收到对应的消息 message 我们该怎么区分究竟是哪个人的?
演示结果如下:
但是其实这里有个小 bug,就是当我们退出重新再登录用户的时候,服务器并没有删掉我们这个用户,但是我们再启动的时候端口号其实已经发生改变了,而服务器保存我的端口号还是老的,这个时候我们,再发消息,服务器可以收到,但是用户收不到
如果我今天上线的话,如果不发信息的话,就不知道我上线了,我也收不到信息,那么能不能把其改成保证一上线就能收到消息
结果如下:
我们这里一般都是用 Ctrl + C 退出用户,但是用户的确退出了,但是服务器并不知道用户退出了,因此我们这里就可以用 Signal 来捕捉退出信号,捕捉信号可以参考之前的这篇内容
【Linux】:进程信号(信号概念 & 信号处理 & 信号产生)
https://island.blog.csdn.net/article/details/143662179UdpClientMain.cc 修改如下:
ClientQuit 全局实现如下:
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);
}
然后在 UdpServer.hpp 服务器端我们也要实现一个移除用户的功能
先定义移除用户的类型别名
using remove_t = std::function<void(InetAddr &id)>;
User.hpp 修改如下:
#pragma once
#include <iostream>
#include <string>
#include <list>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <algorithm>
#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;
};
// 对用户消息进行路由
// UserManage 把所有用户先管理起来
// 把一个新用户添加到在线用户列表,一旦要发信息,我们的 UserManage 不做发送,他要调的就是 User 提供的公共方法
// 这种设计模式就称为 观察者模式 -> observer
class UserManage
{
public:
UserManage()
{
}
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)); // 构建 User 对象
PrintUser();
}
void DelUser(InetAddr &id)
{
// 写法 1
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();
// // 写法 2 -- 不对
// for(auto &user : _online_user)
// {
// if(*user == id){
// _online_user.erase(user);
// break; // 必须 break,避免迭代器失效问题
// }
// }
// 在基于范围的 for 循环中,迭代器是隐式管理的。
// 当调用 erase 后,迭代器 user 失效,但循环仍然尝试继续使用它(即使你使用了 break,某些编译器或情况下仍可能导致问题)。
// // 写法 3
// for (auto it = _online_user.begin(); it != _online_user.end(); ) {
// if (**it == id) {
// it = _online_user.erase(it); // erase 返回下一个有效的迭代器
// } else {
// ++it; // 继续遍历
// }
// }
}
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();
}
}
~UserManage()
{}
private:
std::list<std::shared_ptr<UserInterface>> _online_user; // 在线用户
Mutex _mutex;
};
然后在服务器端 实现相应功能
最终演示结果如下:
这里我们还有个小细节,一般我们的服务器都不想被拷贝或继承,这里我们就写个基类,然后UdpServer 继承它即可,就不用把拷贝私有化了,直接继承这个基类就行
🐋 ping
🐋 netstat
netstat 是一个用来查看网络状态的重要工具 语法: netstat [选项] 功能:查看网络状态 常用选项:
1. n拒绝显示别名,能显示数字的全部转化成数字
2. I仅列出有在 Listen(监听)的服務状态
3. p 显示建立相关链接的程序名
4. t(tcp)仅显示 tcp 相关选项
5. u(udp)仅显示 udp 相关选项
6. a(all)显示所有选项,默认不显示 LISTEN 相关
// 每个 1s 执行一次 netstat -nuap
$ watch -n 1 netstat -nuap
🐋 pidof
// 查看进程 id 的两种方式,显然第二种更简便
lighthouse@VM-8-10-ubuntu:~$ ps ajx | head -1 && ps axj | grep server_udp
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
2648810 2649427 2649427 2648810 pts/2 2649427 S+ 1001 0:00 ./server_udp 8888
2638397 2649450 2649449 2638397 pts/3 2649449 S+ 1001 0:00 grep --color=auto server_udp
lighthouse@VM-8-10-ubuntu:~$ pidof server_udp
2649427
// 杀死进程的两种方式
lighthouse@VM-8-10-ubuntu:~$ kill -9 2649427
lighthouse@VM-8-10-ubuntu:~$ pidof server_udp | xargs kill -9
打个比方:
🥑 上面只介绍基于IPv4的socket网络编程,sockaddr in 中的成员 structin addrsin addr表示32位的IP地址,但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在 字符串表示 和 in_addr 表示 之间转换;
字符串转 in addr 的函数:
in addr 转字符串的函数:
其中 inet_pton 和 inet_ntop 不仅可以转换IPv4 的 in_addr ,还可以转换 IPv6 的 in6_addr,因此函数接口是 void *addrptr
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include<arpa/inet.h>
int main(){
struct sockaddr_in addr:
inet_aton("127.0.0.1",&addr.sin_addr);
uint32_t* ptr =(uint32_t*)(&addr.sin_addr);
printf("addr:%x\n",*ptr);
printf("addr_str:%s\n",inet_ntoa(addr.sin_addr));
return 0;
}
🔥 inet_ntoa 这个函数返回了char*,很显然是这个函数自己在内部为我们申请了一块内存来保存ip 的结果,那么是否需要调用者手动释放呢?
man 手册上说,inet_ntoa 函数是把这个返回结果放到了静态存储区,这个时候不需要我们手动进行释放。那么问题来了,如果我们调用多次这个函数,会有什么样的效果呢?参见如下代码:
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
struct sockaddr_in addr1;
struct sockaddr_in addr2;
addr1.sin_addr.s_addr = 0;
addr2.sin_addr.s_addr = 0xffffffff;
char* ptr1 = inet_ntoa(addr1.sin_addr);
char* ptr2 = inet_ntoa(addr2.sin_addr);
printf("ptr1:%s,ptr2:%s\n", ptr1, ptr2);
return 0;
}
运行结果如下:
在 APUE 中, 明确提出 inet_ntoa 不是线程安全的函数
Udp_client.cc
#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
#include <WinSock2.h> // Windows Sockets API 的头文件,方便使用套接字进行网络通信
#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 tmp;
int len = sizeof(tmp);
int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&tmp, &len);
if (s > 0)
{
buffer[s] = 0;
std::cout << buffer << std::endl;
}
}
closesocket(sockfd);
WSACleanup();
return 0;
}
在 winsock2.h 中定义了一些重要的数据类型和函数,如:
WSAStartup 函数
WSAStartup 函数是 Windows Sockets API 的初始化函数,它用于初始化Winsock库。该函数在应用程序或 DLL 调用任何 Windows 套接字函数之前必须首先执行,它扮演着初始化的角色。 以下是 WSAStartup 函数的一些关键点:
验证结果如下:
🔥 我们在进行 通信验证的时候,在打包到另一个用户上的时候,运行 client_udp ,输入对应信息的时候, server_udp 文件可能没有反应,这个原因是因为我们的云服务器防火墙问题,因此我们就需要进入我们的云服务器,找到防火墙一栏,进行添加规则,如下:
【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【Linux】的内容,请持续关注我 !💞!