【Linux网络编程】Socket-UDP
🔥个人主页:Quitecoder
🔥专栏:linux笔记仓
目录
- 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);
参数说明
参数 | 类型 | 说明 |
---|---|---|
sockfd | int | 套接字文件描述符 |
buf | const void* | 要发送的数据缓冲区 |
len | size_t | 要发送的数据长度 |
flags | int | 控制发送行为的标志位 |
dest_addr | const struct sockaddr* | 目标地址信息 |
addrlen | socklen_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);
参数 | 类型 | 说明 |
---|---|---|
sockfd | int | 套接字文件描述符 |
buf | void* | 接收数据的缓冲区 |
len | size_t | 缓冲区长度 |
flags | int | 控制接收行为的标志位 |
src_addr | struct sockaddr* | 发送方地址信息(可选) |
addrlen | socklen_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);}}
}
所以,服务器首先创建套接字,绑定地址信息,接着就不断地进行收和发即可
客户端
客户端核心步骤
- 创建 Socket:与服务器相同
- 设置目标地址:指定服务器地址信息
- 发送数据:使用
sendto()
向服务器发送请求 - 接收响应:使用
recvfrom()
接收服务器回复 - 关闭 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;
}