当前位置: 首页 > news >正文

【Linux网络编程】Socket-UDP

Alt

🔥个人主页Quitecoder

🔥专栏linux笔记仓

Alt

目录

    • 01.封装UdpSocket
        • 创建套接字
        • 绑定地址信息
        • 收发消息
        • 客户端
    • 02.实现一个英译汉词典
    • 03.通过线程池实现转发业务
      • 客户端修改

01.封装UdpSocket

makefile:

.PHONY:all
all:udpserver udpclient
udpserver:UdpServerMain.ccg++ -o $@ $^ -std=c++11
udpclient:UdpClientMain.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f udpclient udpserver

首先我们为了让服务禁止拷贝,写一个禁止拷贝的基类,让我们的服务端类来继承这个类:

class nocopy
{
public:nocopy() = default;nocopy(const nocopy&) = delete;nocopy& operator=(const nocopy&) = delete;nocopy(nocopy&&) = delete;nocopy& operator=(nocopy&&) = delete;virtual ~nocopy() = default;
};class UdpServer:public nocopy
{
public:UdpServer(){}~UdpServer(){}void InitServer(){}void Start(){}
private:
};

在这里插入图片描述

int socket(int domain, int type, int protocol);

domain,可以选择AF_INET:IPv4 协议,AF_INET6:IPv6 协议,AF_UNIX:本地套接字(进程间通信)

type这里是套接字方案,我们UDP选择SOCK_DGRAM,面向数据报
在这里插入图片描述

第三个protocal,协议编号,我们一般设置为0

一个socket创建通信的一端,创建成功返回新的文件描述符

在这里插入图片描述
未来收发消息的参数,涉及到sockfd,这就是我们创建的套接字

创建套接字
void InitServer()
{//1.创建socket套接字_sockfd =socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){LOG(FATAL,"socket error\n");exit(SOCKET_ERROR);}LOG(DEBUG,"socket created successfully, sockfd=%d\n", _sockfd);
}

完成初始化创建函数并验证:
在这里插入图片描述

绑定地址信息

在这里插入图片描述

//2.绑定地址信息
struct sockaddr_in local;
memset(&local, 0, sizeof(local));//清空结构体
local.sin_family = AF_INET; //地址族
local.sin_port = htons(_localport); //端口号,主机序列转为网络序列
local.sin_addr.s_addr = inet_addr(_localip.c_str()); //IP地址,4字节,网络序列,这里有专门的函数解决
int n=::bind(_sockfd,(const sockaddr*)&local,sizeof(local));//绑定地址信息
if(n < 0)
{LOG(FATAL,"bind error\n");exit(BIND_ERROR);
}
LOG(DEBUG,"bind success, localport=%d\n", _sockfd);

在这里插入图片描述
四字节ip地址的处理,这里我们用了一个库函数一次解决了两个问题

在这里插入图片描述

收发消息

sendto 函数详解

sendto 是用于通过套接字发送数据的系统调用,主要用于 无连接协议( UDP)的通信中。它允许指定目标地址,适合向特定端点发送数据。

函数原型

#include <sys/types.h>
#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);

参数说明

参数类型说明
sockfdint套接字文件描述符
bufconst void*要发送的数据缓冲区
lensize_t要发送的数据长度
flagsint控制发送行为的标志位
dest_addrconst struct sockaddr*目标地址信息
addrlensocklen_t地址结构体长度

返回值

  • 成功:返回实际发送的字节数(可能小于请求的 len
  • 失败:返回 -1,并设置 errno

标志位(flags)

常用的标志位选项:

标志说明
0默认行为
MSG_DONTWAIT非阻塞发送
MSG_CONFIRM确认路径有效性(Linux特有)
MSG_MORE后面还有更多数据(减少报文数量)

recvfrom 函数详解

recvfrom 是一个用于从套接字接收数据的系统调用,主要用于 无连接协议(UDP)的通信中。它不仅能接收数据,还能获取发送方的地址信息

#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);
参数类型说明
sockfdint套接字文件描述符
bufvoid*接收数据的缓冲区
lensize_t缓冲区长度
flagsint控制接收行为的标志位
src_addrstruct sockaddr*发送方地址信息(可选)
addrlensocklen_t*地址结构体长度(输入输出参数)

返回值

  • 成功:返回接收到的字节数
  • 失败:返回 -1,并设置 errno
  • 连接关闭:返回 0(对于面向连接的协议)
void Start()
{_isRunning = true;char inbuffer[1024];while (_isRunning){struct sockaddr_in peer;memset(&peer, 0, sizeof(peer));socklen_t len = sizeof(peer);ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){inbuffer[n] = 0; // 当字符串std::string echo_string = "[udp_server] #";echo_string += inbuffer;sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);}}
}

所以,服务器首先创建套接字,绑定地址信息,接着就不断地进行收和发即可

客户端

客户端核心步骤

  1. 创建 Socket:与服务器相同
  2. 设置目标地址:指定服务器地址信息
  3. 发送数据:使用 sendto() 向服务器发送请求
  4. 接收响应:使用 recvfrom() 接收服务器回复
  5. 关闭 Socket:通信完成后关闭

客户端与服务器的关键区别

步骤服务器客户端
地址绑定必须绑定固定端口通常不绑定(系统自动分配临时端口
地址设置只需设置本地地址必须设置目标服务器地址
通信发起被动接收主动发起
典型流程先接收后发送先发送后接收

客户端不需要显示地bind自己的IP和端口,特殊场景需要绑定固定端口

客户端在首次向服务器发送数据的时候,OS会自动给client bind它自己的IP和端口

所以客户端创建好套接字后,进行收发消息即可

//客户端在未来一定要知道服务器的IP地址和端口号
//./udpclient server-ip server-port
//./udpclient 127.0.0.1 8888
int main(int argc,char*argv[])
{if(argc!=3){std::cerr<<"Usage:"<<argv[0]<<"server-ip server-port"<<std::endl;exit(0);}std::string serverip=argv[1];uint16_t serverport=std::stoi(argv[2]);int sockfd = ::socket(AF_INET,SOCK_DGRAM,0);if(sockfd<0){std::cerr<<"create socket error"<<std::endl;exit(1);}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());struct sockaddr_in recserver;memset(&server,0,sizeof(server));socklen_t len= sizeof(recserver);while(1){std::string line;std::cout<<"Please Enter# ";std::getline(std::cin,line);int n=sendto(sockfd,line.c_str(),line.size(),0,(struct sockaddr*)&server,sizeof(server) );//要知道服务器的地址if(n>0){char buffer[1024];int m =recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&recserver,&len);if(m>0){buffer[m]=0;std::cout<<buffer<<std::endl;}else{break;}}}return 0;
}

云服务器不能绑定自己的公网IP,也不能绑定内网IP,绑定内网IP后就收不到外界的消息

服务器IP一般我们指定为0,服务器bind了任意IP

// local.sin_addr.s_addr = inet_addr(_localip.c_str()); // IP地址,4字节,网络序列,这里有专门的函数解决
local.sin_addr.s_addr=INADDR_ANY;

在服务器编程中,将IP地址绑定为0.0.0.0(通过INADDR_ANY常量表示)是一个关键设计决策

绑定INADDR_ANY意味着:我不关心具体IP是什么,只要是通过我的端口发送到本机的数据,我都接收

在这里插入图片描述

服务器现在可以进行通信了,但是我们现在只能进行打印信息,我们想让服务器来完成一些功能

02.实现一个英译汉词典

服务器内部设置一个回调函数:

using func_t = function<std::string(const std::string)>;

类型为返回值为string

在这里插入图片描述

#pragma once
#include<iostream>
#include<string>
#include<fstream>
#include<unordered_map>
#include"Log.hpp"using namespace log_ns;
const static std::string sep=": ";
class Dict
{
private:void LoadDict(const std::string& path){std::ifstream in(path);if(!in.is_open()){LOG(FATAL,"open %s failed\n",path.c_str());exit(1);}std::string line;while(std::getline(in,line)){LOG(DEBUG,"load info: %s,success\n",line.c_str());if(line.empty())continue;auto pos = line.find(sep);if(pos == std::string::npos)continue;std::string key = line.substr(0,pos);std::string value = line.substr(pos+sep.size());_dict.insert(std::make_pair(key,value));}LOG(INFO,"load %s,done\n",line.c_str());in.close();}
public:Dict(const std::string &dict_path):_dict_path(dict_path){LoadDict(_dict_path);}std::string Translate(std::string word){if(word.empty())return "None";auto iter = _dict.find(word);if(iter == _dict.end()) return "None";else return iter->second;}~Dict(){}
private:std::unordered_map<std::string,std::string> _dict;std::string _dict_path;
};

再修改服务器逻辑即可

#include"UdpServer.hpp"
#include<memory>
#include"Dict.hpp"
using namespace std;
using namespace log_ns;
int main(int argc,char*argv[])
{if(argc!=2){std::cerr<<"Usage:"<<argv[0]<<"local-ip local-port"<<std::endl;exit(0);}uint16_t port=std::stoi(argv[1]);EnableScreen();Dict dict("./dict.txt");func_t translate = std::bind(&Dict::Translate,&dict,std::placeholders::_1);unique_ptr<UdpServer> usvr = make_unique<UdpServer>(translate,port);usvr->InitServer();usvr->Start();return 0;
}

在这里插入图片描述

03.通过线程池实现转发业务

UDP读写都用同一个sockfd,说明UDP是全双工的

using func_t =std::function<void(int,const std::string& message,InetAddr & who)>;

将来这个回调函数的三个参数分别对应sockfd,传过来的信息和用户信息,我们这里用IP+Port来区分用户信息

在这里插入图片描述

void Start()
{_isRunning = true;char message[1024];while (_isRunning){struct sockaddr_in peer;memset(&peer, 0, sizeof(peer));socklen_t len = sizeof(peer);ssize_t n = recvfrom(_sockfd, message, sizeof(message) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){InetAddr addr(peer);message[n] = 0;std::cout << "[" << addr.Ip() << ":" << addr.Port() << "]# " << message << std::endl;//_func(_sockfd, message, addr);}}
}

接受到消息直接调用函数

下面完成聊天的业务逻辑

#pragma once#include<iostream>
#include<string>
#include<vector>
#include"InetAddr.hpp"
#include <sys/socket.h>
#include<sys/types.h>class Route
{
public:Route(){}void CheckOnlineUser(InetAddr &who){for(auto & e:_online_user){if(e == who) return;}_online_user.push_back(who);}void Offline(InetAddr &who){auto iter = _online_user.begin();for(;iter != _online_user.end();iter++){if(*iter == who){_online_user.erase(iter);break;}}}void ForwardHelper(int sockfd,const std::string& message){for(auto &user : _online_user){struct sockaddr_in peer =user.Addr();sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&peer,sizeof(peer));}}void Forward(int sockfd,const std::string& message, InetAddr &who){//1.该用户是否在用户列表中,如果不在,自动添加到在线用户列表CheckOnlineUser(who);if(message== "Q" ||message== "QUIT"){Offline(who);}ForwardHelper(sockfd,message);}~Route(){}
private:std::vector<InetAddr> _online_user;
};class InetAddr
{
private:void ToHost(const struct sockaddr_in &addr){_port = ntohs(addr.sin_port);_ip = inet_ntoa(addr.sin_addr);}public:InetAddr(const struct sockaddr_in &addr):_addr(addr){ToHost(addr);}std::string Ip(){return _ip;}uint16_t Port(){return _port;}~InetAddr(){}struct sockaddr_in Addr(){return _addr;}bool operator == (const InetAddr &addr){return this->_ip == addr._ip && this->_port == addr._port;}
private:std::string _ip;uint16_t _port;struct sockaddr_in _addr;
};

这个代码适用于简单的聊天室或消息广播系统,其中:

  • 所有用户都能看到所有消息

  • 用户通过发送特定消息来加入/离开

  • 不需要复杂的身份验证或权限管理

InetAddr 类封装了网络地址信息,包括IP地址和端口号,并提供了相关的操作方法。

  • 构造函数:从 sockaddr_in 结构体初始化,并转换字节序

  • Ip() 和 Port():获取IP和端口信息

  • Addr():返回原始的 sockaddr_in 结构体

工作流程

  • 用户上线:当用户发送第一条消息时,自动将其添加到在线用户列表

  • 消息转发:所有消息都会被转发给所有在线用户

  • 用户下线:当用户发送"Q"或"QUIT"消息时,将其从在线列表中移除

  • 状态维护:维护一个在线用户列表,用于消息广播

当前的消息转发是在主线程中直接进行的(假设是在一个循环中接收消息并调用Route的Forward方法)。当有多个客户端同时发送消息,或者消息处理(比如转发给很多在线用户)比较耗时的时候,主线程可能会被阻塞,导致无法及时处理新的消息

使用线程池可以将消息转发的任务交给线程池中的工作线程去执行,这样主线程可以继续接收新的消息,提高并发处理能力。

具体来说,我们可以将以下任务放入线程池中执行:

  • 将消息转发给所有在线用户(ForwardHelper操作)

但是需要注意,在线用户列表(_online_user)是共享资源,多个线程同时读写需要加锁保护。

因此,结合线程池后,我们可以这样设计:

  • 主线程接收消息,然后将消息转发任务(包括用户地址、消息内容)包装成一个任务对象,提交给线程池。

  • 线程池中的工作线程执行这个任务:将消息转发给所有在线用户。

同时,由于在线用户列表可能被多个线程同时访问(比如主线程在添加新用户,工作线程在读取用户列表进行转发),我们需要对在线用户列表的访问进行同步。

void Forward(int sockfd,const std::string& message, InetAddr &who){//1.该用户是否在用户列表中,如果不在,自动添加到在线用户列表CheckOnlineUser(who);if(message== "Q" ||message== "QUIT"){Offline(who);}//ForwardHelper(sockfd,message);task_t t =std::bind(&Route::ForwardHelper,this,sockfd,message);ThreadPool<task_t>::GetInstance()->Equeue(t);}

在这里插入图片描述

客户端修改

客户端将来有两个部分,发送消息,发送给放服务器,接受消息,网络获取信息

int InitClient()
{int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "create socket error" << std::endl;exit(1);}return sockfd;
}void RecvMessage(int sockfd, const std::string &name)
{while (true){struct sockaddr_in peer;socklen_t len = sizeof(peer);char buffer[1024];int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){buffer[n] = 0;std::cerr << buffer << std::endl;}else{std::cerr << "recvfrom error" << std::endl;break;}}
}void SendMessage(int sockfd, std::string serverip, uint16_t serverport, const std::string &name)
{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());std::string cli_profix = name + "# "; // sender-thread# 你好while (true){std::string line;std::cout << cli_profix;std::getline(std::cin, line);int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));if (n <= 0)break;}
}int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);int sockfd = InitClient();Thread recver("recver-thread", std::bind(&RecvMessage, sockfd, std::placeholders::_1));Thread sender("sender-thread", std::bind(&SendMessage, sockfd, serverip, serverport, std::placeholders::_1));recver.Start();sender.Start();recver.Join();sender.Join();::close(sockfd);return 0;
}

文章转载自:

http://KOX8dzl5.ptwrz.cn
http://d46J0xle.ptwrz.cn
http://qfFU7leE.ptwrz.cn
http://Ea8XFJLW.ptwrz.cn
http://rVSXPgn1.ptwrz.cn
http://PZpGA3Eg.ptwrz.cn
http://E8mUOq0i.ptwrz.cn
http://DG63LTyy.ptwrz.cn
http://GJSCoe6j.ptwrz.cn
http://WWBNW2Yu.ptwrz.cn
http://k0HRfTrj.ptwrz.cn
http://qhKchTtN.ptwrz.cn
http://YFSFty9D.ptwrz.cn
http://6mvFJRpR.ptwrz.cn
http://bFp4mwr2.ptwrz.cn
http://wo3ys1FG.ptwrz.cn
http://TXicp0lJ.ptwrz.cn
http://NFKw7T7k.ptwrz.cn
http://e9v1MNv6.ptwrz.cn
http://8mmL00zA.ptwrz.cn
http://S9hfyLJ9.ptwrz.cn
http://6onLPYqo.ptwrz.cn
http://ZzgUfHBx.ptwrz.cn
http://sgJrVJx3.ptwrz.cn
http://prxll4az.ptwrz.cn
http://MJfOf1rQ.ptwrz.cn
http://YL6Yz6qx.ptwrz.cn
http://VyzkZ9Jx.ptwrz.cn
http://xLOVHJrP.ptwrz.cn
http://RwQ7JcHZ.ptwrz.cn
http://www.dtcms.com/a/385699.html

相关文章:

  • OpenCV物体跟踪:从理论到实战的全面解析
  • Linux:线程同步
  • Day24_【深度学习(3)—PyTorch使用(2)—张量的数值计算】
  • 9月15日
  • 【langchain】构建简单检索问答链
  • 简单的数组
  • ENVI系列教程(四)——图像几何校正
  • 数据结构基础--散列表
  • 【Redis】-- 主从复制
  • 输入1.8V~5.5V 输出28V DCDC升压芯片TLV61046A
  • Windows 上安装 FFmpeg 8.0(2025 版)——从“手动解压”到“一条命令”的进化之路
  • 红黑树(RBTree)知识总结
  • 若依框架前端通过 nginx docker 镜像本地运行
  • 二十、瑞萨RZT2N2 PROFINET SDK正式发布
  • SpringAI框架接入Deepseek和豆包实现智能聊天
  • 江协科技STM32课程笔记(一) —GPIO
  • 江协科技STM32课程笔记(二)—外部中断EXTI
  • 科技信息差(9.15)
  • 龙珠KS6 10.5T矿机评测:性能、功耗、噪音与冷却分析
  • 打工人日报#20250915
  • 新一代车载诊断框架简介
  • 05-索引-性能分析
  • 【数据工程】 2. Unix 基础与文件操作
  • 第四课、 TypeScript 中 Cocos 的生命周期
  • 联邦学习论文分享:DPD-fVAE
  • Pairwise排序损失:让机器学会排序的艺术
  • 硬件开发—IMX6ULL裸机—UART通信
  • 蓝牙上位机开发指南
  • 【课堂笔记】复变函数-1
  • 谈谈人大金仓数据库