在上一章中,我们已经学过了socket的接口,本章,我们先实现简单的UDP通信,并过渡到需要连接,更有难度的
其中客户端与服务端的创建套接字,转换等操作相似,后面我们可以再进行封装,解耦,使代码逻辑更清晰
UdpServer.hpp
bash
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
// #include "InetAddr.hpp"
using namespace LogModule;
int defaultsockfd = -1;
class UdpServer
{
public:
UdpServer(std ::string &ip, uint16_t port)
: _sockfd(defaultsockfd), _ip(ip), _port(port), _isrunning(false)
{
}
void Init()
{
// 1. 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket0 error!";
exit(1);
}
LOG(LogLevel::INFO) << "socket success,sockfd: " << _sockfd;
// 2. 绑定socket信息,ip和端口, ip(比较特殊,后续解释)
// 2.1 填充sockaddr_in结构体
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 初始化
// 我会不会把我的IP地址和端口号发送给对方?
// IP信息和端口信息,一定要发送到网络!
// 本地格式->网络序列
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO
// IP也是如此,1. IP转成4字节 2. 4字节转成网络序列 -> in_addr_t inet_addr(const char *cp);
// 那么为什么服务器端要显式的bind呢?IP和端口必须是众所周知且不能轻易改变的!
// 此时只开在了栈上,要通过系统调用写入内核
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n == -1)
{
LOG(LogLevel::FATAL) << "bind fail";
exit(2);
}
LOG(LogLevel::INFO) << "bind success!";
}
void Start()
{
while (_isrunning)
{
_isrunning = true;
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1. 收消息, client为什么要个服务器发送消息啊?不就是让服务端处理数据。
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
// 发送消息
int peer_port = ntohs(peer.sin_port); // 从网络拿到的网络序列转换
std::string peer_ip = inet_ntoa(peer.sin_addr); /// 4字节网络风格的IP -> 点分十进制的字符串风格的IP
buffer[n] = 0;
LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port << "]# " << buffer; // 1. 消息内容 2. 谁发的??
// 2. 发消息
std::string echo_string = "server echo#";
echo_string += buffer;
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, sizeof(peer));
}
else
{
LOG(LogLevel::FATAL) << "recvfrom fail";
exit(1);
}
}
}
~UdpServer() {}
private:
int _sockfd;
uint16_t _port;
std::string _ip;
// 用的是字符串风格,点分十进制, "192.168.1.1"
bool _isrunning;
};
UdpClient.cc
bash
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include"Log.hpp"
using namespace LogModule;
int main(int args,char *argv[])
{
if(args!=3)
{
std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}
// 1. 创建socket
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0)
{
std::cerr<<"socket fail"<<std::endl;
return 2;
}
// 2. 本地的ip和端口是什么?要不要和上面的"文件"关联呢?
// 问题:client要不要bind?需要bind.
// client要不要显式的bind?不要!!首次发送消息,OS会自动给client进行bind,OS知道IP,端口号采用随机端口号的方式
// 为什么?一个端口号,只能被一个进程bind,为了避免client端口冲突
// client端的端口号是几,不重要,只要是唯一的就行!
// 填写服务器信息
std::string server_ip=argv[1];
std::uint16_t server_port=std::stoi(argv[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 Enter# ";
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),0,(struct sockaddr*)&peer,&len);
if(m>0)
{
buffer[m]=0;
std::cout<<buffer<<std::endl;
}
}
return 0;
}
注意事项
_ip地址是使用的string,但进行绑定时,却是要是数字,原因 数字就分成4个,每个数字表示0到255 只需4字节
bind时,服务器要显示绑定,但客服端不用,OS会随机绑定,防止(OS知道IP,随机绑定端口号)你绑定的会占用其他进程
UdpServer.cc
bash
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
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]); //xiecuo
Enable_Console_Log_Strategy();
std::unique_ptr<UdpServer> usvr =std::make_unique<UdpServer>(ip,port);
usvr->Init();
usvr->Start();
return 0;
}
实验不足,修改
实验发现,绑定的ip地址为服务器公网IP无用,是因为我们OS并未按照公网IP
云服务器可以通过映射,将我们的内网IP与公网IP一一对应只有内网IP
本地环回,要求c s必须在同一条机器,表面我们是本地通信,client发送的数据,不会被推送到网络,而是在OS转一圈发给对应的服务端(经常用来进行网络代码测试)
bind不能绑定公网IP
bind 绑定内网IP,或者本地环回IP,客户端使用另一个依旧无法通信,是因为如果我们显示进行地址bind,client访问,就必须使用servet绑定的,所以不推荐,修改
bash// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO local.sin_addr.s_addr = INADDR_ANY;而我们传输是为了让服务端进行处理数据的,那么我们也可以不再传ip了,而是传回调函数,进行处理(使用functional),这样就能进行处理了
而修改后,客户端无论是绑定本地环回ip还是内网ip都可以两者相互交流
bash#include <iostream> #include <memory> #include <functional> #include "UdpServer.hpp" std::string defaulthandler(const std::string &message) { std::string hello = "hello, "; hello += message; return hello; } 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]); //xiecuo Enable_Console_Log_Strategy(); std::unique_ptr<UdpServer> usvr =std::make_unique<UdpServer>(port,defaulthandler); usvr->Init(); usvr->Start(); return 0; }UdpServer.hpp
bash#pragma once #include <iostream> #include <string> #include <functional> #include <strings.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 namespace LogModule; using func_t =std::function<std::string(const std::string&)>; int defaultsockfd = -1; class UdpServer { public: UdpServer( uint16_t port,func_t func) : _sockfd(defaultsockfd), _func(func), _port(port), _isrunning(false) { } void Init() { // 1. 创建套接字 _sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (_sockfd < 0) { LOG(LogLevel::FATAL) << "socket0 error!"; exit(1); } LOG(LogLevel::INFO) << "socket success,sockfd: " << _sockfd; // 2. 绑定socket信息,ip和端口, ip(比较特殊,后续解释) // 2.1 填充sockaddr_in结构体 struct sockaddr_in local; bzero(&local, sizeof(local)); // 初始化 // 我会不会把我的IP地址和端口号发送给对方? // IP信息和端口信息,一定要发送到网络! // 本地格式->网络序列 local.sin_family = AF_INET; local.sin_port = htons(_port); // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO local.sin_addr.s_addr = INADDR_ANY; // IP也是如此,1. IP转成4字节 2. 4字节转成网络序列 -> in_addr_t inet_addr(const char *cp); // 那么为什么服务器端要显式的bind呢?IP和端口必须是众所周知且不能轻易改变的! // 此时只开在了栈上,要通过系统调用写入内核 int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local)); if (n == -1) { perror("bind:"); LOG(LogLevel::FATAL) << "bind fail"; exit(2); } LOG(LogLevel::INFO) << "bind success!"; } void Start() { _isrunning=true; while (_isrunning) { _isrunning = true; char buffer[1024]; struct sockaddr_in peer; socklen_t len = sizeof(peer); // 1. 收消息, client为什么要个服务器发送消息啊?不就是让服务端处理数据。 ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); if (n > 0) { // 发送消息 int peer_port = ntohs(peer.sin_port); // 从网络拿到的网络序列转换 std::string peer_ip = inet_ntoa(peer.sin_addr); /// 4字节网络风格的IP -> 点分十进制的字符串风格的IP buffer[n] = 0; LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port << "]# " << buffer; // 1. 消息内容 2. 谁发的?? // 2. 发消息 std::string echo_string = "server echo#"; echo_string += buffer; sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, sizeof(peer)); } else { LOG(LogLevel::FATAL) << "recvfrom fail"; exit(1); } } } ~UdpServer() {} private: int _sockfd; uint16_t _port; // std::string _ip; // 用的是字符串风格,点分十进制, "192.168.1.1" func_t _func; bool _isrunning; };
使用的相关接口
bash
NAME
send, sendto, sendmsg - send a message on a socket
SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
bash
NAME
recv, recvfrom, recvmsg - receive a message from a socket
SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
bash
INET(3) Linux Programmer's Manual INET(3)
NAME
inet_aton, inet_addr, inet_network, inet_ntoa, inet_makeaddr, inet_lnaof, inet_netof - Internet address manipulation routines
SYNOPSIS
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
in_addr_t inet_network(const char *cp);
char *inet_ntoa(struct in_addr in);
struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host);
in_addr_t inet_lnaof(struct in_addr in);
in_addr_t inet_netof(struct in_addr in);
bash
NAME
bind - bind a name to a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
bash
NAME
inet_aton, inet_addr, inet_network, inet_ntoa, inet_makeaddr, inet_lnaof, inet_netof - Internet address manipulation routines
SYNOPSIS
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
in_addr_t inet_network(const char *cp);
char *inet_ntoa(struct in_addr in);
struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host);
in_addr_t inet_lnaof(struct in_addr in);
而上面的makefile进行的操作,可以在多用户间进行通信,即你可以与你好友通信
bash
.PHONY:all
# 修正目标名拼写(upserver → udpserver)
all:udpclient udpserver
# 修正编译命令:添加-o选项
udpclient:UdpClient.cc
g++ -o $@ $^ -std=c++17 -lpthread
udpserver:UdpServer.cc
g++ -o $@ $^ -std=c++17 -lpthread
# 可选:添加清理目标
.PHONY:clean
clean:
rm -f udpclient udpserver
🔥个人主页: Milestone-里程碑
❄️个人专栏: <<力扣hot100>> <<C++>><<Linux>>
🌟心向往之行必能至