目录
UDP是基于socket(基于IP和port进行通信就叫做socket通信)进行网络通信的,那我们这篇博客就来介绍一下基于UDP通信的基本流程,先让服务端和客户端进行简单的跨网络通信。
服务器
首先我们需要创建UDP套接字,用到的接口是
man socket
如果要使用UDP通信,第一个参数就传
它用来表示基于网络通信,第二个参数就传
它用来表示是面向数据报,第三个参数我们就默认传0就可以了,因为前两个参数就可以确定它就是UDP协议。它的返回值我们可以认为是一个文件描述符,未来我们就可以向这个文件中去写入或读取东西
接下来是bind socket和网络信息
man 2 bind
第一个参数就是前面接口的返回值,第二个参数是输入性参数,是一个结构体的指针,所以我们要创建这样一个结构体并且填充内容,第三个参数就是第二个参数的大小
所以在绑定之前,我们需要创建struct sockaddr的对象并且填充内容:
因为socket编程是有不同种类的,而操作系统又是拿C语言写的,为了统一接口,我们就用struct sockaddr这个类型,具体到网络通信是struct sockaddr_in这个类型,所以我们创建的是struct sockaddr_in这个类型的变量,到时候进行强制类型转换成struct sockaddr类型
local的类型是一个结构体,结构体中有这些类型:
我们可以看到这里面有port,就是我们传进来的端口号,这个端口号当然也要进行网络传输,因为放到网络中的数据必须是大端,所以我们有接口用来主机序列转网络序列
有了这些接口,我们就可以任意的16位,32位,主机序列转网络序列,网络序列转主机序列。
传完了端口号,就要传ip地址了,就是sin_addr的内容,同样这个也需要主机序列转网络序列,但同时我们知道从命令行参数传进来的IP地址是字符串风格的点分十进制的格式(比如193.168.111.222,这样占15个字节),这样占的空间就大,我们知道每一位都是0~255,这用一个字节来表示整数足够了(这样只占四个字节),所以我们要将字符串风格的点分十进制转换成4字节IP。上面两个工作可以通过下面的接口全部完成
man inet_addr
填充完了我们就可以进行bind了,到此初始化的工作就完成了
下面就是不断的从网络中获取信息,我们可以设置成死循环的形式,我们先实现这样的场景,谁给我们发的,我们再发回去
UDP是面向数据报的,从网络中获取信息的接口是:
man recvfrom
第二个参数是要把从网络中获得的信息存到哪,后两个参数是输出型参数,里面就存放着这个消息是谁发来的一些信息
我们再把消息发回去的接口是:
man sendto
后两个参数就存放着要把消息发送给谁
这样,服务器大致就写好了,还有一些细节需要处理
我们运行主函数的代码时命令行参数就要写上ip和端口号
客户端
客户端要运行主函数肯定是要加上你想访问的服务器的ip地址和端口号,因为这样可以在互联网中唯一确定一个进程,客户端和服务器一样,也是要创建socket,但是不用显示bind,就是我们不要bind,客户端第一次向服务器发消息的时候操作系统会自动bind,因为如果客户端显示的bind,那就意味着不同的客户端可能用一个端口号(比如手机中的抖音和淘宝用一个端口号),这样就导致两个客户端无法同时启动,就会出现端口号冲突的问题。那么就意味着客户端的代码要比服务器要好写
测试
我们像下面这样运行服务器
它是可以成功的,127.0.0.1这个IP我们称为本地环回,它不将服务发送到网络中,可以用于本地通信,常用于代码测试,于是此时我们再起一个SSH渠道就可以实现本地的服务器和客户端通信了
这是本地环回,毕竟不是跨网络的,我们把IP换成服务器的公网IP试一试
可以看到它会报错,因为我们的服务器,无法直接bind公网IP(云服务器不允许),我们也严重不推荐绑定任意一个确定的IP(虚拟机可以)
因为一台设备可能有多个IP地址,我向每个IP地址发,进程都应该收到,如果bind特定的IP地址,那么只有这个IP地址可以收到,所以我们一般传0.0.0.0,简写成0,表示任意IP地址bind,所以之前的代码中还是不要传IP了,因为确定是0
这样,客户端给本地环回和公网IP就都可以发消息了
网络命令
还有一些网络命令我们补充一下:
ping 用来检测网络连通性,加上-c3(count=3)表示检测三次
netstat -puan 用来查udp网络服务
p代表process进程,u代表udp,a代表all,n代表number,就是能显示成数字的都显示成数字
同样,我们要查tcp网络服务是netstat -tlnp
t就表示tcp,l表示状态是listen的
watch -n 1 ls表示每个一秒执行一次ls命令,我们就可以用它来查网络服务
我们之前要查一个进程的pid要这样:
ps ajx | head -1 && ps ajx | grep server非常繁琐,于是我们可以用
pidof server就可以直接查到进程的pid
我们如果想通过pid杀掉进程就可以
pidof server | xargs kill -9
这里的xargs就是把管道中的东西放到-9后面
code for Udp_echo_serve
//UdpServer.hpp
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cerrno>
#include<cstdlib>
#include <cstring>
#include "Log.hpp"
enum error
{
SOCKET_ERR = 1,
BIND_ERR,
USAGE_ERR,
};
class UdpServer
{
public:
UdpServer(uint16_t port) : _port(port), _socketfd(-1)
{
}
void InitServer()
{
// 1.创建UDP socket套接字
_socketfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socketfd < 0)
{
LOG(FATAL, "socket error,%d,%s", errno, strerror(errno));
exit(SOCKET_ERR);
}
LOG(INFO, "create socket success,socket:%d", _socketfd);
// 填充struct sockaddr_in结构
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 对空间进行清0
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr=INADDR_ANY;//这个数就是0,因为是0,大小端也就没意义了
// local.sin_addr.s_addr = inet_addr(_ip.c_str());
int n = bind(_socketfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(FATAL, "bind error,%d,%s", errno, strerror(errno));
exit(BIND_ERR);
}
LOG(INFO, "bind success");
}
void start()
{
while (true)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
ssize_t n=recvfrom(_socketfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(n>0)
{
buffer[n]=0;
LOG(DEBUG,"get message from[%s,%d]:%s",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port),buffer);//这样解析就知道是谁发过来的
sendto(_socketfd,buffer,strlen(buffer),0,(struct sockaddr*)&peer,len);
}
}
}
private:
int _socketfd;
// std::string _ip;
uint16_t _port;
};
//Main.cc
#include<iostream>
#include"UdpServer.hpp"
void Usage(char* arg)
{
std::cout<<"Usage: please enter:"<<std::endl;
std::cout<<arg<<' '<<"server port"<<std::endl;
}
int main(int argc,char*argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
Enablescreen();
uint16_t port=std::stoi(argv[1]);
UdpServer udps(port);
udps.InitServer();
udps.start();
return 0;
}
//UdpClient.cc
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
void Usage(char *arg)
{
std::cout << "Usage: please enter:" << std::endl;
std::cout << arg << ' ' << "server ip " << "server port" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
//创建socket套接字
int socketfd = socket(AF_INET, SOCK_DGRAM, 0);
if (socketfd < 0)
{
std::cerr << "socket error" << std::endl;
}
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 message;
while(true)
{
std::cout<<"Please Enter# ";
std::getline(std::cin,message);
sendto(socketfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));//给服务器发消息
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
char buffer[1024];
ssize_t n=recvfrom(socketfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);//接收来自服务器的消息
if(n>0)
{
buffer[n]=0;
std::cout<<"server echo# "<<buffer<<std::endl;
}
}
return 0;
}
Udp_dict_serve
上面我们已经会了实现服务器和客户端之间的一个基本跨网络通信,下面我们可以给服务器部署一些任务,就比如查字典的服务:客户端通过发送一些英文单词给服务器,然后从服务器中获得汉语翻译,于是我们就在上面的代码上简单写一个Dict.hpp来实现这个功能
#include <iostream> #include <string> #include <unordered_map> #include "Log.hpp" const std::string defaultconfpath = "./Dict.txt"; const std::string seq = ": "; class Dict_serve { private: bool Load() { std::ifstream in(_conf_filepath); if (!in.is_open()) { LOG(FATAL, "open %s error", _conf_filepath.c_str()); return false; } std::string line; while (std::getline(in, line)) { if (line.empty()) continue; auto pos = line.find(seq); if (pos == std::string::npos) continue; std::string word = line.substr(0, pos); if (word.empty()) continue; std::string han = line.substr(pos + seq.size()); if (han.empty()) continue; LOG(DEBUG, "load %s %s success", word.c_str(), han.c_str()); _dict.insert({word, han}); } in.close(); LOG(DEBUG, "load %s success", _conf_filepath.c_str()); return true; } public: Dict_serve(const std::string &path = defaultconfpath) : _conf_filepath(path) { Load(); } std::string translate(const std::string &word) { auto iter = _dict.find(word); if (iter == _dict.end()) return "nofind(未找到)"; // return _dict[word]; return iter->second; } private: std::unordered_map<std::string, std::string> _dict; std::string _conf_filepath; };
所有的服务器本质还是解决的是输入输出的问题,我们不想让网络通信模块和翻译(业务)模块强耦合,所以可以这样做:
把一个通用的任务当作udpserver的成员变量,以后就通过这个变量做任务
所以我们在构建服务器对象时这样去构建: