目的:利用udp协议,使用socket接口,编写Linux平台的echo服务器
功能:客户端向服务器发消息,服务器接收并且把消息发回客户端
工具:VS code ,makefile,XShell,腾讯云服务器
准备工作
Makefile
.PHONY:all
all:udp_client udp_surver
udp_client:udp_client.cc
g++ -o $@ $^ -std=c++11
udp_surver:udp_surver.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf udp_client udp_surver
log.txt日志功能
#include<iostream>
#include<string>
#include<ctime>
#include<cstdio>
#include<cstdarg> //用于解析可变参数列表
//日志级别
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
//日志级别转字符串表示
const char* gLevelMap[]={
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
void logMessage(int level,const char* format,...)
{
char stdBuffer[1024];
time_t timestamp=time(nullptr);//获取当前时间戳
snprintf(stdBuffer,sizeof stdBuffer,"[%s][%ld]",gLevelMap[level],timestamp);//将日志信息格式化到标准缓冲区
char logBuffer[1024];//自定义部分缓冲区
va_list args;
va_start(args,format);
vsnprintf(logBuffer,sizeof logBuffer,format,args);
va_end(args); //args=nullptr
printf("%s%s\n",stdBuffer,logBuffer);
}
编写服务器
udp_server.hpp
成员变量
//一个服务器需要ip地址和port端口号
//ip用户输入为字符串,需要后续转为点分十进制网络序列
//sock文件描述符,后续由操作系统分配
uint16_t _port;
std::string _ip;
int _sock;
初始化服务器
![](https://img-blog.csdnimg.cn/img_convert/15ddeb2d4c07465683e5949acc2b7b24.png)
成功返回socket文件描述符,失败返回-1,错误码被设置
domain 填充你要进行本地通信还是网络通信的宏AF_INET 网络通信
type 填充你要面向字节流还是面向数据报的宏SOCK_DGRAM 面向数据报
protocal一般为0
//初始化服务器,系统调用,完成网络功能
//1.创建套接字(ip+port)
_sock=socket(AF_INET,SOCK_DGRAM,0);
if(_sock<0)//差错处理
{
logMessage(FATAL,"%d:%s",errno,strerror(errno));
exit(2);
}
填充port和ip
inet_addr可以让我们的IP地址由char*转为网络的四字节序列
//ip地址,点分十进制,【0-255】:1byte,4个区域,一共4byte可以表示一个ip
struct sockaddr_in local;//用于填充本地port+ip
bzero(&local,sizeof(local));
local.sin_family=AF_INET;//说明用户创建了文件相关套接字,用于网络通信
local.sin_port=htons(_port);//服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络!
local.sin_addr.s_addr=_ip.empty()?INADDR_ANY:inet_addr(_ip.c_str());//服务器在工作时,我们可以让他从任意IP获取数据
//0.0.0.0代表的是任意ip,如果用户穿了就用穿的ip,因为网卡可能有很多个
绑定加差错处理
if(bind(_sock,(struct sockaddr*)&local,sizeof(local))<0)
{
logMessage(FATAL,"%d:%s",errno,strerror(errno));
exit(2);
}
然后初始化服务器就完成了
logMessage(NORMAL,"init udp server done...%s",strerror(errno));
初始化服务器:
bool initServer()
{
//初始化服务器,系统调用,完成网络功能
//1.创建套接字(ip+port)
_sock=socket(AF_INET,SOCK_DGRAM,0);
if(_sock<0)//差错处理
{
logMessage(FATAL,"%d:%s",errno,strerror(errno));
exit(2);
}
//2.bind:用户设置ip和port在内核中与当前进程强相关
//ip地址,点分十进制,【0-255】:1byte,4个区域,一共4byte可以表示一个ip
struct sockaddr_in local;//用于填充本地port+ip
bzero(&local,sizeof(local));
local.sin_family=AF_INET;//说明用户创建了文件相关套接字,用于网络通信
local.sin_port=htons(_port);//服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络!
local.sin_addr.s_addr=_ip.empty()?INADDR_ANY:inet_addr(_ip.c_str());//服务器在工作时,我们可以让他从任意IP获取数据
//0.0.0.0代表的是任意ip,如果用户穿了就用穿的ip,因为网卡可能有很多个
if(bind(_sock,(struct sockaddr*)&local,sizeof(local))<0)
{
logMessage(FATAL,"%d:%s",errno,strerror(errno));
exit(2);
}
logMessage(NORMAL,"init udp server done...%s",strerror(errno));
return true;
}
小结:调用系统接口,创建套接字(文件),以后数据传输用的就是这个文件,设置ip和port
绑定ip+port和fd强相关
服务器开始工作
peer是一个输出型参数,用于接收客户端的ip和端口号的信息
revfrom用于接收远端信息
sendto用于发送数据给远端,用的都是_sock相当于是从套接字处读取和发送信息
void Start()
{
// 作为一款网络服务器,永远不退出的!
// 服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!
// echo server: client给我们发送消息,我们原封不动返回
char buffer[SIZE];//用于接收客户端发来的消息
while(1)
{
//用户端传来的消息中会有用户端的ip/port相关信息
//定义peer 输出型参数用于接收用户信息
//len输入输出型参数
struct sockaddr_in peer;
bzero(&peer,sizeof(peer));
socklen_t len=sizeof(peer);
ssize_t s=recvfrom(_sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(s>0)
{
buffer[s]=0;
//开始解析客户端信息
uint16_t cli_port=ntohs(peer.sin_port);//网络转主机
std::string cli_ip=inet_ntoa(peer.sin_addr);
printf("[%s:%d]# %s\n",cli_ip.c_str(),cli_port,buffer);//在服务端打印客户信息以及客户发来的消息
}
//将客户端发来的消息写回给客户端
sendto(_sock,buffer,strlen(buffer),0,(struct sockaddr*)&peer,len);
}
}
udp_server.cc
// ./udp_server port
int main(int argc,char* argv[])
{
if(argc!=2)
exit(1);
uint16_t port=atoi(argv[1]);
std::unique_ptr<UdpServer> svr(new UdpServer(port));//用智能指针管理
svr->initServer();
svr->Start();
return 0;
}
udp_client.cc
和服务端类似的编写
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <memory>
static void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
<< std::endl;
}
// ./udp_client 127.0.0.1 8080 //你要向谁发消息
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
// client要不要bind??要,但是一般client不会显示的bind,程序员不会自己bind
// client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->
// client 一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port呢??
// client一般不需要显示的bind指定port,而是让OS自动随机选择(什么时候做的呢?)
std::string message;
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
char buffer[1024];
while(true)
{
std::cout << "请输入你的信息# ";
std::getline(std::cin, message);
if(message == "quit") break;
// 当client首次发送消息给服务器的时候,OS会自动给client bind他的IP和PORT
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof server);
//为了接口统一性填充占位符
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr*)&temp, &len);
if(s > 0)
{
buffer[s] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
return 0;
}
效果演示
启动服务器,绑定端口号8080
![](https://img-blog.csdnimg.cn/img_convert/9ec171ce66c849edb85c8790cee19d1c.png)
启动客户端,向执行服务器发送消息(标定服务器的ip和端口号)
![](https://img-blog.csdnimg.cn/img_convert/28c313c6938049f28b196faf7025147b.png)
可以开始发消息了
![](https://img-blog.csdnimg.cn/img_convert/c0263c5f09bf420e8ff61cfa7d4c4c8c.png)
4.拓展(下次更新啊)
同时我们可以将功能扩展为原创XShell,即客户端远程操控服务器,还可以添加多线程,实现群聊功能,就是客户端一边读取服务器发来的消息,一边发消息给服务器,服务器负责将客户端发来的消息发送给每一个向自己发送过消息的客户端
注:127.0.0.1作为本机测试,相当于数据没有进入局域网,也可以开放端口号,实现真正的远端通信,需要对方电脑下载客户端(udp_client),向我的公网ip发送消息,我的服务器便可以收到消息,实现简单的网络通信,但是只有一台电脑,做不了实验了~