目录
准备工作
makefile
udpServer.hpp
udpServer.cc
细节1
服务端部署
创建套接字
接口认识1
socket
协议家族
绑定套接字
认识接口2
bind
sockaddr_in结构体类型
细节2
bzero
inet_addr
服务器启动(初启动)
udpServer.hpp
udpServer.cc
细节3
本地回环通信
认识指令1
netstat
细节4
代码整改
整改后代码
udpServer.hpp
udpServer.cc
认识接口3
recvfrom
参数介绍
inet_ntoa
start启动
客户端部署
认识接口4
sendto
同一台云服务器上
不同的服务器上
通信和业务逻辑解耦
全部代码
udpServer.hpp
udpServer.cc
udpClient.hpp
udpClient.cc
makefile
准备工作
这些先在Xshell上创建,后续直接使用VScode来进行编码
makefile
udpServer.hpp
#pragma once
#include <iostream>
#include <string>
namespace Server
{
class udpServer
{
public:
udpServer()
{
}
void initServer() //初始化
{
}
void start() //启动
{
}
~udpServer() //析构
{
}
};
}
udpServer.cc
#include "udpServer.hpp"
#include <memory>
using namespace std;
using namespace Server;
int main()
{
std::unique_ptr<udpServer> usvr(new udpServer());
usvr->initServer();
usvr->start();
return 0;
}
细节1
服务端部署
创建套接字
接口认识1
socket
参数
返回值
读写网络就像读写文件一样
初始化代码写到构造里面了,后面修改
协议家族
绑定套接字
认识接口2
bind
这里的填充涉及到内存对齐方面,知道就好
sockaddr_in结构体类型
对类型进行了很多层的封装
细节2
从上面看到,我们可以得到一个问题:为什么库里面使用的是整型的ip地址,而我们是用string的ip地址的呢?
点分十进制和整数风格互转
bzero
往一段空间中填 0
inet_addr
记得包含头文件
getopt
附:当我们想要做命令行解析的时候是可以用下面这个接口的,这里我们不使用(参数太少了,没必要使用)
服务器启动(初启动)
至此服务器已经可以正常启动
udpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
namespace Server
{
using namespace std;
static const string defaultIp = "0.0.0.0"; //TODO
enum {USAGE_ERR = 1, SOCKET_ERR, BIND_ERR}; //1.命令行输入错误 2.创建套接字错误 3.绑定端口号错误
class udpServer
{
public:
udpServer(const uint16_t &port, const string& ip = defaultIp): _port(port),_ip(ip),_sockfd(-1)
{
//注意这里是直接写在构造里面的,是写错地方了,虽然运行是没有错的,由于修改图片太麻烦,下面统一进行了修改
//1.创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd == -1)
{
cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
exit(SOCKET_ERR); //创建套接字失败直接终止进程
}
//2.绑定套接字(port, ip)
struct sockaddr_in local; //这里是定义了一个变量,在栈上,而且是用户层,还没有bind之前都是没有产生联系
bzero(&local, sizeof(local)); //先填 0 再修正
//注意这下面几个名字是拼接出来的,就是那个##拼接而来的
local.sin_family = AF_INET; //这里设置与套接字的AF_INET设置意义是不一样的,socket是创建一个网络通信的套接字,在这里是填充一个sockaddr_in的结构体用来网络通信
local.sin_port = htons(_port);//你如果给别人发信息,你的port和ip要不要发送给对方? 答案是要的
local.sin_addr.s_addr = inet_addr(_ip.c_str()); //1. string->unit32_t 2. htonl(); -> inet_addr
int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if(n == -1)
{
cerr << "bin error: " << errno << " : " << strerror(errno) << endl;
exit(BIND_ERR);
}
//UDP Server 的预备工作完成
}
void initServer() // 初始化
{
}
void start() // 启动
{
//服务器的本质其实就是一个死循环,
for(;;)
{
sleep(1);
}
}
~udpServer() // 析构
{
}
private:
uint16_t _port;
string _ip; // TODO
int _sockfd;
};
}
udpServer.cc
#include "udpServer.hpp"
#include <memory>
using namespace std;
using namespace Server;
static void Usage(string proc)
{
cout << "Usage:\n\t" << proc << " local_ip local_port\n\n"; //命令提示符
}
int main(int argc, char *argv[])
{
if(argc != 3) //这里我们只想要传递两个参数,所以当argc不是3的时候就直接报错退出就行了,注意文件名运行的那个指令也会算进去所以argc +1
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[2]); //使用atoi强转,因为argv里面放置的都是字符串,类型需要转换
string ip = argv[1];
std::unique_ptr<udpServer> usvr(new udpServer(port, ip));
usvr->initServer();
usvr->start();
return 0;
}
认识接口2
细节3
刚开始测试的地址是直接使用这一个
本地回环通信
127.0.0.1
认识指令1
netstat
查看网络的接口 netstat
细节4
假如绑定其他的公网IP地址需要注意
代码整改
所以一个服务器真实情况下是要接受任意ip发过来的通信,因此我们修改ip的,不需要传ip号了
整改后代码
udpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
namespace Server
{
using namespace std;
static const string defaultIp = "0.0.0.0"; // 直接使用这个缺省值
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR
}; // 1.命令行输入错误 2.创建套接字错误 3.绑定端口号错误
class udpServer
{
public:
udpServer(const uint16_t &port, const string &ip = defaultIp) : _port(port), _ip(ip), _sockfd(-1)
{}
void initServer() // 初始化
{
// 1.创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == -1)
{
cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
exit(SOCKET_ERR); // 创建套接字失败直接终止进程
}
// 2.绑定套接字(port, ip)
struct sockaddr_in local; // 这里是定义了一个变量,在栈上,而且是用户层,还没有bind之前都是没有产生联系
bzero(&local, sizeof(local)); // 先填 0 再修正
// 注意这下面几个名字是拼接出来的,就是那个##拼接而来的
local.sin_family = AF_INET; // 这里设置与套接字的AF_INET设置意义是不一样的,socket是创建一个网络通信的套接字,在这里是填充一个sockaddr_in的结构体用来网络通信
local.sin_port = htons(_port); // 你如果给别人发信息,你的port和ip要不要发送给对方? 答案是要的
local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. string->unit32_t 2. htonl(); -> inet_addr
// local.sin_addr.s_addr = htonl(INADDR_ANY); //可以主机转网络,不够也可以不处理,直接赋值也行
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n == -1)
{
cerr << "bin error: " << errno << " : " << strerror(errno) << endl;
exit(BIND_ERR);
}
// UDP Server 的预备工作完成
}
void start() // 启动
{
// 服务器的本质其实就是一个死循环,
for (;;)
{
sleep(1);
}
}
~udpServer() // 析构
{
}
private:
uint16_t _port;
// 实际上,一款网络服务器,不建议指明一个IP,因为一个服务器可以能有多个ip,万一用户使用其他的ip地址来访问该端口号(这里是8080,就收不到了),这也是我们为什么使用0.0.0.0的IP缺省值
string _ip;
int _sockfd;
};
}
udpServer.cc
#include "udpServer.hpp"
#include <memory>
using namespace std;
using namespace Server;
static void Usage(string proc)
{
cout << "Usage:\n\t" << proc << " local_port\n\n"; //命令提示符
}
int main(int argc, char *argv[])
{
if(argc != 2) //这里我们只想要传递两个参数,所以当argc不是3的时候就直接报错退出就行了,注意文件名运行的那个指令也会算进去所以argc +1
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]); //使用atoi强转,因为argv里面放置的都是字符串,类型需要转换
//string ip = argv[1];
std::unique_ptr<udpServer> usvr(new udpServer(port));
usvr->initServer();
usvr->start();
return 0;
}
注意这里是服务端是不需要IP,可以接受任意IP地址发来的请求,但是客户端是需要的,这一点后面再细谈,所以并不会造成淘宝的请求跑到京东去了
这里为什么使用8080的因为服务器可以有很多端口号,当服务器收到了大量的数据,并不是全部都是由一个端口号来进行处理的,也可能是8081之类的端口号,这时候的端口号是没有意义的,后序会详谈,其实不同的端口号是有指定的绑定的不能任意绑定,这是因为只有我自己使用
且无论是UDP还是TCP都是采取这样的形式,接受任意IP的数据,通过端口号来确定谁是谁处理
认识接口3
recvfrom
读取数据
参数介绍
socket_t
inet_ntoa
将一个网络字节序的IP地址(也就是结构体in_addr类型变量)转化为点分十进制的IP地址(字符串)
start启动
void start() // 启动
{
// 服务器的本质其实就是一个死循环
char buffer[gnum]; // 定义一个数组来充当缓冲区
for (;;)
{
// 读取数据
struct sockaddr_in peer;
socklen_t len = sizeof(peer); // 必须设置成这个结构体的大小,当作为输入时,告诉recvfrom的长度的多少
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
// 关系两件事情
// 1.数据是什么? 2. 谁发的?
if (s > 0)
{
buffer[s] = 0;
// 因为是从网络上读取的,所以一定要转,可以使用接口
string clientip = inet_ntoa(peer.sin_addr); // 1.网络序列 2.整数 -> 点分十进制的ip
uint16_t clientport = ntohs(peer.sin_port);
string message = buffer;
cout << clientip << "[" << clientport << "]# " << message << endl;
}
}
}
至此服务器端基本完成,停下来处理客户端
客户端部署
认识接口4
sendto
sendto告诉客户端要发给谁
0~1023在云服务器上已经被绑定了
同一台云服务器上
不同的服务器上
现在无法跨主机发送消息,权限问题,后续解决
sz:下载到本地
rz:上传到服务器
chmod:修改权限
至于如何打开端口后续文章介绍 -- 未完持续
通信和业务逻辑解耦
我们可以添加function来对业务逻辑进行解耦操作,融入下面代码
function对server通信和业务逻辑解耦!
全部代码
udpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <cerrno>
#include <functional>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
namespace Server
{
using namespace std;
static const string defaultIp = "0.0.0.0"; // 直接使用这个缺省值
static const int gnum = 1024;
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR
}; // 1.命令行输入错误 2.创建套接字错误 3.绑定端口号错误
typedef function<void(string, uint16_t, string)> func_t;
class udpServer
{
public:
udpServer(const func_t &cd, const uint16_t &port, const string &ip = defaultIp)
: _callback(cd), _port(port), _ip(ip), _sockfd(-1)
{
}
void initServer() // 初始化
{
// 1.创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == -1)
{
cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
exit(SOCKET_ERR); // 创建套接字失败直接终止进程
}
cout << "udpServer success: "
<< " : " << _sockfd << endl;
// 2.绑定套接字(port, ip)
// 未来服务器要明确的port, 不能随意改变 -- 变了别人就找不到了
struct sockaddr_in local; // 这里是定义了一个变量,在栈上,而且是用户层,还没有bind之前都是没有产生联系
bzero(&local, sizeof(local)); // 先填 0 再修正
// 注意这下面几个名字是拼接出来的,就是那个##拼接而来的
local.sin_family = AF_INET; // 这里设置与套接字的AF_INET设置意义是不一样的,socket是创建一个网络通信的套接字,在这里是填充一个sockaddr_in的结构体用来网络通信
local.sin_port = htons(_port); // 你如果给别人发信息,你的port和ip要不要发送给对方? 答案是要的
local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. string->unit32_t 2. htonl(); -> inet_addr
// local.sin_addr.s_addr = htonl(INADDR_ANY); //可以主机转网络,不够也可以不处理,直接赋值也行
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n == -1)
{
cerr << "bin error: " << errno << " : " << strerror(errno) << endl;
exit(BIND_ERR);
}
// UDP Server 的预备工作完成
}
void start() // 启动
{
// 服务器的本质其实就是一个死循环
char buffer[gnum]; // 定义一个数组来充当缓冲区
for (;;)
{
// 读取数据
struct sockaddr_in peer;
socklen_t len = sizeof(peer); // 必须设置成这个结构体的大小,当作为输入时,告诉recvfrom的长度的多少
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
// 关系两件事情
// 1.数据是什么? 2. 谁发的?
if (s > 0)
{
buffer[s] = 0;
// 因为是从网络上读取的,所以一定要转,可以使用接口
// inet_ntoa 将一个网络字节序的IP地址(也就是结构体in_addr类型变量)转化为点分十进制的IP地址(字符串)
string clientip = inet_ntoa(peer.sin_addr); // 1.网络序列 2.整数 -> 点分十进制的ip
uint16_t clientport = ntohs(peer.sin_port);
string message = buffer;
cout << clientip << "[" << clientport << "]# " << message << endl;
//我们只把数据读上来就完了吗? 我们要对数据进行处理 -- 所以我们用回调函数的方式来解决
_callback(clientip, clientport, message);
}
}
}
~udpServer() // 析构
{
}
private:
uint16_t _port;
// 实际上,一款网络服务器,不建议指明一个IP,因为一个服务器可以能有多个ip,万一用户使用其他的ip地址来访问该端口号(这里是8080,就收不到了),这也是我们为什么使用0.0.0.0的IP缺省值
string _ip;
int _sockfd;
func_t _callback; // 回调函数,用以处理数据
};
}
udpServer.cc
#include "udpServer.hpp"
#include <memory>
using namespace std;
using namespace Server;
static void Usage(string proc)
{
cout << "Usage:\n\t" << proc << " local_port\n\n"; //命令提示符
}
void handlerMessage(string clientip, uint16_t clientport, string message)
{
//这里就可以对message进行特定的业务处理,而不关心message怎么来的 --- 这就是server通信和业务逻辑解耦!
}
int main(int argc, char *argv[])
{
if(argc != 2) //这里我们只想要传递两个参数,所以当argc不是3的时候就直接报错退出就行了,注意文件名运行的那个指令也会算进去所以argc +1
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]); //使用atoi强转,因为argv里面放置的都是字符串,类型需要转换
//string ip = argv[1];
std::unique_ptr<udpServer> usvr(new udpServer(handlerMessage, port));
usvr->initServer();
usvr->start();
return 0;
}
udpClient.hpp
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
namespace Client
{
using namespace std;
class udpClient
{
public:
udpClient(const string &serverip, const uint16_t &serverport)
: _serverip(serverip), _serverprot(serverport), _sockfd(-1), _quit(false)
{
}
void initClient()
{
// 1. 创建socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == -1)
{
cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
exit(1); // 创建套接字失败直接终止进程
}
cout << "socket success: " << " : " << _sockfd << endl; //成功打印出来
// 2. client要不要bind(必须要!)因为bind就是和系统、网络产生联系. client要不要显示的bind -> 需不需要程序员bind? 名字不重要,重要的是唯一性的,和服务端是不一样的
// 就像宿舍号是几不重要,有就行了。一个端口号只能被一个客户端绑定,就像是服务端是明星,客户端是民众,民众名字不重要
// 服务端是具体的一家公司,比如抖音是字节的,就像一个手机有很多app比如抖音,快手这样的客户端,不能让它们固定bind端口号,万一其他公司也用了用一个端口就冲突其他不来了
// 写服务器的是一家公司,写client是无数家公司 -- 有OS自动形成端口进行bind!不需要自己操作,包括ip地址也不需要,OS自己会处理,当然也可以自己写
// 那么OS在什么时候,如何bind
}
void run()
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(_serverip.c_str());
server.sin_port = htons(_serverprot);
string messages;
while (!_quit)
{
cout << "Please Enter# ";
cin >> messages;
sendto(_sockfd, messages.c_str(), sizeof(messages), 0, (struct sockaddr *)&server, sizeof(server));
}
}
~udpClient()
{
}
private:
int _sockfd;
string _serverip;
uint16_t _serverprot;
bool _quit;
};
}
udpClient.cc
#include "udpClient.hpp"
#include <memory>
using namespace Client;
static void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " server_ip server_port\n\n"; //命令提示符
}
// ./udpClient server_ip server_port 第一个是运行方式 要知道要发送的服务端的ip地址 和 端口号
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
unique_ptr<udpClient> ucli(new udpClient(serverip, serverport));
ucli->initClient();
ucli->run();
return 0;
}
makefile
cc=g++
.PHONY:all
all:udpClient udpServer
udpClient:udpClient.cc
$(cc) -o $@ $^ -std=c++11
udpServer:udpServer.cc
$(cc) -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f udpClient udpServer
至于如何打开端口后续文章介绍 -- 未完持续中……