文章目录
好的, 今天我们开始写一个简单的网络代码. 整体代码难度不大, 不过我打算其中穿插着说一点基础知识, 整体可能就比较多了.
1. udp_echo_server是个什么东西?
首先, 我们先来说一下udp_echo_server
这个程序要做什么? 这个是我们开始学习网络的第一个编码实践, 所以说是很简单, 整体的功能是有两个端, 一端是客户端, 一端是服务端, 客户端每次向服务端发送数据, 服务端接受到后吧这个数据又发回客户端, 实现一个客户端数据回显的效果.
然后这个程序使用的网络协议是UDP协议
, 整体很简单.
2. 初步文件构建.
我们先来简单预测一下有哪些文件?
我打算:
UdpServer.hpp
服务端头文件UdpServerMain.cc
服务端主函数UdpClientMain.cc
客户端主函数
不过因为我们可能中间需要多次编译, 所以我们先写一个makefile
脚本文件吧:
哎, 那有人可能会有疑问, 你这个用户端你咋不编译啊? 这是因为我们一上来先写服务端, 用户端等写完了我们再用make编译, 因为现在还没写嘛…
好的, 下面我们开始编码吧.
3. nocopy
服务器防止被拷贝
一般来说, 服务器程序是不会用到拷贝的, 因此为了防止其他人误写拷贝或者错误使用服务端的类, 我们写一个nocopy
, 我们的服务端类可以继承这个类, 从而间接实现服务端不会被拷贝的一个效果.
我花两分钟就编完了, 我把这个类的拷贝构造和赋值运算符重载函数直接delete
掉了:
那下面我们让UdpServer.hpp
中的服务端继承一下:
好的, 这样的话我们的UdpServer
类就不能拷贝了…
之后, 我们来构思一下我们的服务端主函数要实现什么功能?
作为一个服务器, 应该先构建出一个服务类对象, 然后这个类对象需要先初始化, 然后启动起来进行服务.
好的, 我们围绕这个实现功能, 我们来写一写服务端.
4. 创建套接字
一个程序要跨网络通信, 需要套接字, 即socket
, 那我们来给我们的服务端写一个套接字. 套接字socket = ip + port, 实际上套接字就是一个整数, 一个文件描述符, 我们在网络编程的时候万物皆文件, 然后把这个文件和ip以及端口号绑定起来, 表示一个网络当中具有唯一性的进程, 这叫做套接字的作用.
这样就完成了吗? 显然没有啊兄弟, 这样你只是在栈上定义出来了一个类型, 一个空间而已, 系统中没有真正的文件的兄弟, 想要有真正的文件, 我们需要调用接口socket
. 来, 让我们看看对应的接口介绍:
int socket(int domain, int type, int protocol);
- domain: 域, 用来指定该socket是网络通信还是本地通信.
- AF_UNIX Local communication unix(7)
- AF_INET IPv4 Internet protocols ip(7)
- type: 套接字类型
- SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
- protocol 协议类型
- 因为我们前面指明了
AF_INET
和SOCK_DGRAM
, 因此可以写0为默认.
- 因为我们前面指明了
- 返回值:
- success 返回文件描述符
- 失败, -1, 错误码被设置
好的, 所以我们写这样:
好的, 显然我们的exit是红色的, 所以我们把头文件给他包上.
好的, 我们再来考虑一下如果socket创建失败了? 那我们是不是需要终止这个进程呀? 因为文件命名符都要不过来了, 后面肯定是没办法继续的嘛. 如果socket创建成功了, 那么咱们就打印一个std::cout << "DEBUG :socket create success, _sockfd: " << _sockfd << std::endl;
用来当作debug信息. 我们预期这个sockfd是3, 为什么? 因为0是标准输入, 1是标准输出, 2是标准错误, 所以下一个系统肯定给咱们的程序分配一个3给到sockfd.
基于这个预测, 我们来测试一下.
看来是符合我们预期的, 那我们继续? 我们创建好了一个sockfd文件描述符, 但是我们的套接字已经弄好了吗?
显然没有啊兄弟, 现在只是创建了这么一个文件和文件描述符而已, 咱们现在需要把这个sockfd关联上ip地址和prot.
所以, 下一步是bind
操作.
5. sockfd bind ip & port
接下来我们继续写我们的bind操作哈. 我们先来看一下bind接口所需要的参数是啥?
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd: 套接字描述符
- sockaddr: 绑定的ip + port信息
- addrlen: 绑定的ip + port结构体的大小.
- 返回值是如果失败返回-1, 错误码被设置, 成功返回0.
所以, 我们可以get到, 我们绑定ip + port, 就需要用一个struct sockaddr_in
类型的结构体给他套进去.
请注意, 这个地方用的是struct sockaddr_in
而非struct sockaddr
, 这是一个类似于多态的接口, struct sockaddr*
是一个通用指针, 然后struct sockaddr_in
是专门用来处理网络通信的ip
& port
等信息的结构体.
我们来认识一下struct sockaddr_in
这个结构体:
不过好像有个小问题啊? 咋有个"不允许使用不完整的类型"的报错呢? 这是因为没有包头文件, 我们查找一下对应的头文件.
上面这个地方需要额外注意, 就是我们在填充sockadd_in
类型的时候, 我们这个类型中的数据将来是要发到网络当中的, 所以需要把主机字节序转换成网络字节序. 说白了就是大小端问题.
其中, 对于ip地址, 需要把ip地址从字符串形式转换成4字节的形式, 然后再转成网络字节序, 对于port, 需要转网络字节序.
自己做这个工作很麻烦, 还需要判断大小端, 然后再进行转换, 我们有对应的库函数去做这个事情, 转ip: inet_addr()
以及 转port: htons()
.
下面是两个接口的截图:
但是代码中local.sin_family = AF_INET;
是啥玩意呢? 是一个标志位, 用来标识struct sockaddr_in
是将来用来网络通信的还是本地进程通信的. AF_INET
标识是网络通信.
那到这里是绑定成功了吗? 没有啊兄弟, 我们只是填充好了一个在用户栈上的一个变量而已, 下面开始绑定, 把sockfd 与 对应的ip和port绑定起来.
咱们来解释一下为啥用ip -> "127.0.0.1"来进行测试, 这个是因为这个ip比较特殊, 是一个本地回环, 就是在本主机内部通讯的ip地址, 用这个ip如果通过了, 则证明我们的软件内部是没问题的, 再上线网络, 如果这个都过不了, 则问题一定在软件内部.
然后, 我们运行测试一下:
看来没啥大问题哈, 我们看来是绑定成功了.
所以呢, 我们创建套接字然后绑定完了ip和port, 这才算是为网络通信做了铺垫, 下面我们得开始写我们的服务逻辑了.
6. start 开始服务
首先搞明白一个问题, 就是服务器是个啥玩意? 这个东西是一个24小时不断运行的死循环代码. 所以我们的进程一旦开始, 就不会主动退出.
所以我们的Start()
逻辑应该是这样的:
写到这里, 我们需要想我们的服务器要干嘛的? 接受数据包, 然后把这个数据包扔回客户端的!
要接受数据包, 需要用接口 -> recvfrom()
, 这个地方咋不是read()
呢? 因为UDP协议是面向数据报的, 而非字节流的.
我们来简单介绍一下这个recvfrom()
接口.
- sockfd: sockfd.
- buf: 缓冲区
- len: 缓冲区大小
- flags: 接受标识符, (0是阻塞接受)
- src_addr: 输入输出参数, 用来保存接收到的数据报的客户端信息.
- addrlen: 输入输出参数, 用来保存接收到的数据报的客户端信息的大小.
我们拿到了数据呢? 之后我们是不是要在服务器端打印一下, 看看是不是真的拿到了, 然后再构造一个返回给客户端的数据? 如果n <= 0, 咱们直接std::cout << "recvfrom , error" << std::endl; // debug 信息.
即可.
好的, 我们大体逻辑写好了吧? 然后把析构写一下: 要记得关掉文件描述符.
我们现在可以测试吗? 不能啊兄弟, 没有客户端怎么测试呢? 我们先快速写一个客户端代码出来:
7. 编码客户端
我们的客户端也需要跨网络发数据吧? 所以, 也是第一步弄一个sockfd
.
然后呢, 我们要发给服务器, 所以我们需要提前"内置"好服务器的套接字吧?
这个地方需要注意, 我们的客户端是不需要绑定端口号的ip地址的, 为啥呢? 为啥服务端需要绑定端口号和ip地址, 而我们的客户端不需要绑定端口号和ip地址?
兄弟, 你想想看啊, 服务器在公司手里, 服务端是运行在服务器上的, 一般一个主机上是只运行那个服务的, 所以端口号基本不会改变嘛, 但是客户端呢? 运行在用户的手机上吧? 用户开多少个应用你知道吗? 你不知道, 那有没有各种应用的端口号如果让程序员指定会不会冲突呢? 肯定会啊, 所以说客户端让用户的OS分配就好了, 程序员是不需要明确写的.
那, 有人可能会有疑问, 那么服务器怎么找到用户的用户端呢? 首先, 是客户端先给服务器发消息, 第二就是服务器拿到消息之后, 就会拿到这次客户端的具体ip地址和port.
所以, 我们的客户端应该不断发消息, 所以写一个循环嘛, 然后发成功了, 就阻塞等待接受消息, 如果收到了, 咱们就打印一遍(回显), 如果接受失败, 就break出去. 如果一上来就没发成功, 我们也是break, 然后打个提示信息, 好找错误.
我们测试一下吧?
不过, 我们先把makefile在写一下, 因为现在是一个客户端和一个服务端同时编译了.
输入: netstat -aupn
看来好像是能跑了~
但是, 还是稍微存在一点细节的, 下面来进行细说:
8. 服务端ip应该绑0号pi.
实际上, 我们的服务端是由很多ip地址的, 有内网, 有公网, 我们尝试用客户端以内网的形式发送信息给服务端.
我们看一下这台机器的内网ip:
在linux机器上输入: ifconfig
我们启动一下看一下结果:
这是为啥呢? 因为我们的服务端绑定的不是内网啊, 服务端有很多个ip, 但是只能通过绑定的ip给他发信息, 是不太好的, 我们希望服务端的任何ip的数据包都可以被拿到. 所以:
我们给服务端绑定ip地址为0号ip(0.0.0.0). 看来我们需要修改一下代码:
9. 加入日志
为了输入一些debug和error信息比较方便, 我们可以写一个日志加进去, 之前是写好的了, 所以我们直接CV到代码里就好了.
我们再来看一下效果:
然后我们还可以把那个ip + port封装起来, 可以让他自动转为主机序列, 而不用在外面转换.
10. 封装ip + port类
// InetAddr.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
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()
{
}
private:
std::string _ip;
uint16_t _port;
struct sockaddr_in _addr;
};
我们把封装好的这个ip + port弄到服务端代码中去:
好的, 终于差不多了, 当然也有一些细节是没有处理好的, 或者说想要处理好细节, 还需要很多代码进行补充, 这次只是一个练习, 也就不求甚解了~
11. reference code
UdpServer.hpp
#pragma once
#include "nocopy.hpp"
#include <sys/socket.h> // 主要 socket 函数和类型
#include <stdlib.h>
#include <iostream>
#include <string.h>
#include <netinet/in.h>
#include <string>
#include <arpa/inet.h>
#include <unistd.h>
#include "LockGuard.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
using namespace log_ns;
static const int gsockfd = -1;
static const int glocalport = 8899;
enum
{
SOCKET_ERROR = 1,
BIND_ERROR
};
class UdpServer : public nocopy
{
public:
UdpServer(std::string localip) : _sockfd(gsockfd), _localport(glocalport), /*_localip(localip),*/ _isrunning(false)
{
}
void InitServer()
{
// 创建sockfd文件
_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
// std::cout << "FATAL: socket error" << std::endl;
LOG(FATAL, "socket error\n");
exit(SOCKET_ERROR);
}
// std::cout << "DEBUG :socket create success, _sockfd: " << _sockfd << std::endl; // 3"
LOG(DEBUG, "socket create success, _sockfd: %d\n", _sockfd); // 3
// bind ip & port
struct sockaddr_in local;
memset(&local, 0, sizeof(sockaddr_in));
local.sin_family = AF_INET;
/*local.sin_addr.s_addr = inet_addr(_localip.c_str());*/
local.sin_addr.s_addr = INADDR_ANY; // 服务器端,进行任意IP地址绑定, INADDR_ANY -> 0, 表示绑定任意端口号
local.sin_port = htons(_localport);
int n = bind(_sockfd, (sockaddr *)&local, sizeof(local));
if (n < 0)
{
// std::cout << "FATAL, bind error" << std::endl;
LOG(FATAL, "bind error\n");
exit(BIND_ERROR);
}
// std::cout << "DEBUG, socket bind success" << std::endl;
LOG(DEBUG, "socket bind success\n");
}
void Start()
{
_isrunning = true;
char inbuffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
while (_isrunning)
{
int n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer), 0, (sockaddr *)&peer, &len);
if (n > 0)
{
// 转换成主机序列
InetAddr addr(peer);
// uint16_t _port = ntohs(peer.sin_port);
// std::string _ip = inet_ntoa(peer.sin_addr);
inbuffer[n] = 0;
// std::cout << "[" << _ip << ":" << _port << "]# " << inbuffer << std::endl; // 打印一下消息
std::cout << "[" << addr.Ip() << ":" << addr.Port() << "]# " << inbuffer << std::endl; // 打印一下消息
// 制造要发给客户端的数据
std::string echo_string = "[udp_server echo] #";
echo_string += inbuffer;
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
}
else
{
std::cout << "recvfrom , error" << std::endl; // debug 信息.
}
}
}
~UdpServer()
{
if (_sockfd > gsockfd)
::close(_sockfd);
}
private:
int _sockfd;
uint16_t _localport;
// std::string _localip;
bool _isrunning;
};
UdpServerMain.cc
#include"UdpServer.hpp"
#include<memory>
int main()
{
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>("127.0.0.1"); //C++14的标准
usvr->InitServer();
usvr->Start();
return 0;
}
UdpClientMain.cc
#include <stdlib.h>
#include <sys/socket.h>
#include <iostream>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
int main()
{
int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "create socket error" << std::endl;
exit(1);
}
// 客户端的socket文件会自动绑定ip地址和端口号, 我们无需关心
// 你要发给谁?
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(8899);
server.sin_addr.s_addr = inet_addr("10.0.16.4");
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)
{
// 收消息
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024]; // 缓冲区
int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &len);
if(m > 0)
{
buffer[m] = 0; // 写好字符串截至的位置
std::cout << buffer << std::endl;
}
else
{
std::cout << "recvfrom error" << std::endl; // debug
break;
}
}
else // 没有发送出去
{
std::cout << "sendto error" << std::endl;
break;
}
}
::close(sockfd);
return 0;
}
nocopy.hpp
#pragma once
class nocopy
{
public:
nocopy(){}
~nocopy(){}
nocopy(const nocopy&) = delete;
const nocopy& operator=(const nocopy&) = delete;
};
Log.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <ctime>
#include <cstdarg>
#include <fstream>
#include <cstring>
#include <pthread.h>
#include "LockGuard.hpp"
namespace log_ns
{
enum
{
DEBUG = 1,
INFO,
WARNING,
ERROR,
FATAL
};
std::string LevelToString(int level)
{
switch (level)
{
case DEBUG:
return "DEBUG";
case INFO:
return "INFO";
case WARNING:
return "WARNING";
case ERROR:
return "ERROR";
case FATAL:
return "FATAL";
default:
return "UNKNOWN";
}
}
std::string GetCurrTime()
{
time_t now = time(nullptr);
struct tm *curr_time = localtime(&now);
char buffer[128];
snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",
curr_time->tm_year + 1900,
curr_time->tm_mon + 1,
curr_time->tm_mday,
curr_time->tm_hour,
curr_time->tm_min,
curr_time->tm_sec);
return buffer;
}
class logmessage
{
public:
std::string _level;
pid_t _id;
std::string _filename;
int _filenumber;
std::string _curr_time;
std::string _message_info;
};
#define SCREEN_TYPE 1
#define FILE_TYPE 2
const std::string glogfile = "./log.txt";
pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;
// log.logMessage("", 12, INFO, "this is a %d message ,%f, %s hellwrodl", x, , , );
class Log
{
public:
Log(const std::string &logfile = glogfile) : _logfile(logfile), _type(SCREEN_TYPE)
{
}
void Enable(int type)
{
_type = type;
}
void FlushLogToScreen(const logmessage &lg)
{
printf("[%s][%d][%s][%d][%s] %s",
lg._level.c_str(),
lg._id,
lg._filename.c_str(),
lg._filenumber,
lg._curr_time.c_str(),
lg._message_info.c_str());
}
void FlushLogToFile(const logmessage &lg)
{
std::ofstream out(_logfile, std::ios::app);
if (!out.is_open())
return;
char logtxt[2048];
snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s",
lg._level.c_str(),
lg._id,
lg._filename.c_str(),
lg._filenumber,
lg._curr_time.c_str(),
lg._message_info.c_str());
out.write(logtxt, strlen(logtxt));
out.close();
}
void FlushLog(const logmessage &lg)
{
// 加过滤逻辑 --- TODO
LockGuard lockguard(&glock);
switch (_type)
{
case SCREEN_TYPE:
FlushLogToScreen(lg);
break;
case FILE_TYPE:
FlushLogToFile(lg);
break;
}
}
void logMessage(std::string filename, int filenumber, int level, const char *format, ...)
{
logmessage lg;
lg._level = LevelToString(level);
lg._id = getpid();
lg._filename = filename;
lg._filenumber = filenumber;
lg._curr_time = GetCurrTime();
va_list ap;
va_start(ap, format);
char log_info[1024];
vsnprintf(log_info, sizeof(log_info), format, ap);
va_end(ap);
lg._message_info = log_info;
// 打印出来日志
FlushLog(lg);
}
~Log()
{
}
private:
int _type;
std::string _logfile;
};
Log lg;
#define LOG(Level, Format, ...) \
do \
{ \
lg.logMessage(__FILE__, __LINE__, Level, Format, ##__VA_ARGS__); \
} while (0)
#define EnableScreen() \
do \
{ \
lg.Enable(SCREEN_TYPE); \
} while (0)
#define EnableFILE() \
do \
{ \
lg.Enable(FILE_TYPE); \
} while (0)
};
LockGuard.hpp
#pragma once
#include <pthread.h>
class LockGuard
{
public:
LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
{
pthread_mutex_lock(_mutex);
}
~LockGuard()
{
pthread_mutex_unlock(_mutex);
}
private:
pthread_mutex_t *_mutex;
};
InetAddr.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
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()
{
}
private:
std::string _ip;
uint16_t _port;
struct sockaddr_in _addr;
};
makefile
.PHONT: all
all: udpserver udpclient
udpserver: UdpServerMain.cc
g++ -o $@ $^ -std=c++14
udpclient: UdpClientMain.cc
g++ -o $@ $^ -std=c++14
.PHONY:clean
clean:
rm -rf udpserver udpclient