Linux网络编程【UDP网络通信demon】
1. server.hpp
下面实现的基于UDP协议的简单的网络通信:
我们需要一个服务器server.cc和客户端client.cc,用server.hpp封装一个简单的服务器。
在服务器当中,我们需要设计初始化服务器init接口和启动服务器start接口。
> init
在初始化服务器的时候,我们首先需要创建Socket。所以我们要认识一个接口socket:
我们来看到这个接口的返回值是一个文件描述符,因此,socket实际上再系统内部为我们创建了一个网络文件。所以,其实网络的本质也是文件,不过我们现在还不能真正理解这句话。我们只要知道创建socket的时候我们把网卡打开了就足以支撑我们后续写代码了。
// 服务端初始化和启动void init(){// 创建套接字->打开网络文件_sfd = socket(AF_INET, SOCK_DGRAM, 0);if(_sfd<0){LOG(log_level::Fatal)<<"socket 创建失败!";exit(1);}}
接下来,我们需要绑定端口号和IP地址。因为,我作为服务器的话,在和另一台主机交互时,是一定要把自己的主机地址(IP地址)和进程地址(端口号)交给客户端的,这样客户端才可以找到服务器进程。
所以,我们要认识下一个接口bind:
第二个参数我们需要填写sockaddr_in的成员:
我们用户层使用的IP地址都是形如192.12.1.1的字符串风格,但是如果是字符串的话,在网络中传输所占的字节太大了。其实,4个字节就足以记录一个IP地址,这就需要string和整形之间的转化了。不过,还需要做一步格式的转化。这里系统为我们提供了inet_addr接口可以直接帮助我们完成这两步。
至此,UDP服务端的初始化就完成了
// 服务端初始化和启动void init(){// 创建套接字->打开网络文件_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(log_level::Fatal) << "socket 创建失败!";exit(1);}LOG(log_level::Info) << "socket 创建成功!" << _sockfd;// 填充sockaddr_in结构体struct sockaddr_in local;// 数据清空bzero(&local, sizeof(local));// 数据将来一定会发送到网络// 端口号和IP地址 本地格式->网络序列local.sin_family = AF_INET; // 协议家族local.sin_port = htons(_port); // 端口号local.sin_addr.s_addr = inet_addr(_ip.c_str()); // IP地址int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(log_level::Fatal) << "bind 失败!";exit(2);}LOG(log_level::Info) << "bind 成功!" << _sockfd;}
> start
服务端启动后是一个死循环,需要不断的接收客户端发送的信息,因为是无连接的,所以就无脑收,和无脑发就完事了。
要做到收发消息,我们需要用到下面接口recvfrom和sendto,这两个接口都非常简单。
void start(){_isrunning = true;while (_isrunning){char buffer[1024];sockaddr_in local;socklen_t len = sizeof(local);// 收信息ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&local, &len);if (n > 0)//成功是收到信息{buffer[n] = 0;LOG(log_level::Info) << buffer;// 发信息(简答一点,稍作修改)std::string send_string = "server say@";send_string += buffer;ssize_t m = sendto(_sockfd, send_string.c_str(), send_string.size(), 0, (struct sockaddr *)&local, sizeof(local));}}}
2. server.cc
接下来,我们就来实现服务端的启动启程,我们在命令行中./server IP port来启动我们的服务器。所以我们如下简单设计一下即可:
#include <iostream>
#include "server.hpp"
#include <memory>int main(int argc, char *argv[])
{if (argc != 3){std::cout << "Usage: ./server IP port" << std::endl;return 1;}using_screen_strategy();std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);std::unique_ptr<server> p1 = std::make_unique<server>(ip, port);p1->init();p1->start();return 0;
}
3. client.cc
首先,客户端知道服务器的IP地址和端口号,这是因为我们用的APP和所对应的服务器都是一家公司做的。所以 ,服务器的IP地址和端口号都是内置的。
客户端不需要显示的bind自己的IP地址和端口号,但是,在首次发送信息的时候,系统会自动绑定IP地址和端口号,端口号是系统随机分配的。
为什么要这样做呢??
首先,端口号原则上来说只能有一个进程占有,我们的一台主机上可以同时启动了多个进程。
比如我先后打开了抖音和快手两个APP,如果这两个进程都显示的绑定系统的端口号,有没有可能这两个进程所绑定端口号是一样的呢??所以,为了避免端口号冲突,系统会为我们分配随机未使用的端口号自动绑定。
#include <iostream>
#include "server.hpp"
#include <memory>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string>
#include <string.h>
#include <netinet/in.h>
#include "log.hpp"
#include <arpa/inet.h>// 客户端知道服务器的IP地址和端口号
int main(int argc, char *argv[])
{if (argc != 3){std::cout << "Usage: ./server IP port" << std::endl;return 1;}using_screen_strategy();std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);// 创建套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){LOG(log_level::Fatal) << "socket 创建失败!";exit(1);}LOG(log_level::Info) << "socket 创建成功!" << sockfd;// 客户端不需要显示的bind自己的IP地址和端口号// 但是,在首次发送信息的时候,系统会自动绑定IP地址和端口号,端口号是系统随机分配的// 为什么要这样做呢??首先,端口号原则上来说只能有一个进程占有,我们的一台主机上可以同时启动了多个进程// 比如我先后打开了抖音和快手两个APP,如果这两个进程都显示的绑定系统的端口号,有没有可能这连个进程所绑定// 端口号是一样的呢??所以,为了避免端口号设计,系统会为我们分配随机未使用的端口号自动绑定。// 填写服务器信息sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = port;local.sin_addr.s_addr = inet_addr(ip.c_str());// 收发送消息while (true){std::string input;std::cout << "请输入:";std::getline(std::cin, input);// 发送消息int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&local, sizeof(local));// 接收消息【有可能连接多个服务器,所以要接收服务器端口号】char buffer[1024];sockaddr_in peer;socklen_t len = sizeof(peer);int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (m > 0){buffer[m] = 0;std::cout << buffer << std::endl;}}return 0;
}
4. 本地环回测试代码
目前我们的服务端是不能直接bind我们远端的云服务器的,所以,我们通过本地环汇来进行我们网络代码的测试:
就是这个IP地址,我们来测试一下:
本地环回:要求我们的客户端和服务端都在一台主机上,表明我们是本地通信。客户端发送的信息不回直接推送到网络中,而是操作系统内部转一圈,然后直接交给对应的服务器端。
其实,我们也可以通过内网IP来进行网络通信,这里就不演示了。
5. INADDR_ANY(绑定任意IP)
如果我们的服务器端绑定的是本地环汇IP,但是客户端用内网IP去给服务器端发送消息。那么,服务端就无法收到客户端发送的报文了。因为客户端bind了唯一的IP地址,但是我们主机上有两个IP地址,一个是内网IP,一个是本地环回。我们的服务器一个端口可能需要从多个IP地址中获取报文进行处理。所以,这种服务器bind死一个IP地址的做法我们是非常不推荐的!!
我们一般这样bind:
验证结果如下:
INADDR_ANY其实就是一个宏,本质就是0。
至此,我们的网络通信模块就已近大功告成了,不过我们接下来还需要添加一些服务模块和网络模块进行解耦来完成一些基于网络通信的基本服务。下一篇博客详说啦~~
6. 源码
log日志类和mutex锁的代码前面有,这里就不贴出来了。
server.hpp
#pragma once#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string>
#include <string.h>
#include <netinet/in.h>
#include "log.hpp"
#include <arpa/inet.h>using namespace log_module;const int default_sockfd = -1;class server
{
public:server(uint16_t port): _sockfd(default_sockfd),// _ip(ip),_port(port),_isrunning(false){}~server(){}// 服务端初始化和启动void init(){// 创建套接字->打开网络文件_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(log_level::Fatal) << "socket 创建失败!";exit(1);}LOG(log_level::Info) << "socket 创建成功!" << _sockfd;// 填充sockaddr_in结构体struct sockaddr_in local;// 数据清空bzero(&local, sizeof(local));// 数据将来一定会发送到网络// 端口号和IP地址 本地格式->网络序列local.sin_family = AF_INET; // 协议家族local.sin_port = htons(_port); // 端口号// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // IP地址local.sin_addr.s_addr = INADDR_ANY; // IP地址任意绑定int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(log_level::Fatal) << "bind 失败!";exit(2);}LOG(log_level::Info) << "bind 成功!" << _sockfd;}void start(){_isrunning = true;while (_isrunning){char buffer[1024];sockaddr_in local;socklen_t len = sizeof(local);// 收信息ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&local, &len);if (n > 0) // 成功是收到信息{buffer[n] = 0;LOG(log_level::Info) << buffer;// 发信息(简答一点,稍作修改)std::string send_string = "server say@";send_string += buffer;ssize_t m = sendto(_sockfd, send_string.c_str(), send_string.size(), 0, (struct sockaddr *)&local, sizeof(local));}}}private:int _sockfd;// std::string _ip; // IP地址用的字符串风格uint16_t _port; // 端口号bool _isrunning;
};
server.cc
#include <iostream>
#include "server.hpp"
#include <memory>int main(int argc, char *argv[])
{if (argc != 2){std::cout << "Usage: ./server port" << std::endl;return 1;}using_screen_strategy();// std::string ip = argv[1];uint16_t port = std::stoi(argv[1]);std::unique_ptr<server> p1 = std::make_unique<server>(port);p1->init();p1->start();return 0;
}
client.cc
#include <iostream>
#include "server.hpp"
#include <memory>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string>
#include <string.h>
#include <netinet/in.h>
#include "log.hpp"
#include <arpa/inet.h>// 客户端知道服务器的IP地址和端口号
int main(int argc, char *argv[])
{if (argc != 3){std::cout << "Usage: ./server IP port" << std::endl;return 1;}using_screen_strategy();std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);// 创建套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){LOG(log_level::Fatal) << "socket 创建失败!";exit(1);}LOG(log_level::Info) << "socket 创建成功!" << sockfd;// 客户端不需要显示的bind自己的IP地址和端口号// 但是,在首次发送信息的时候,系统会自动绑定IP地址和端口号,端口号是系统随机分配的// 为什么要这样做呢??首先,端口号原则上来说只能有一个进程占有,我们的一台主机上可以同时启动了多个进程// 比如我先后打开了抖音和快手两个APP,如果这两个进程都显示的绑定系统的端口号,有没有可能这连个进程所绑定// 端口号是一样的呢??所以,为了避免端口号冲突,系统会为我们分配随机未使用的端口号自动绑定。// 填写服务器信息sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = inet_addr(ip.c_str());// 收发送消息while (true){std::string input;std::cout << "请输入#";std::getline(std::cin, input);// 发送消息int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&local, sizeof(local));// 接收消息【有可能连接多个服务器,所以要接收服务器端口号】char buffer[1024];sockaddr_in peer;bzero(&peer, 0);socklen_t len = sizeof(peer);int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (m > 0){buffer[m] = 0;std::cout << buffer << std::endl;std::cout << "------------------------------\n";}}return 0;
}
Makefile
.PHNOY:all
all:server clientserver:server.ccg++ -o $@ $^ -std=c++17client:client.ccg++ -o $@ $^ -std=c++17.PHNOY:clean
clean:rm -rf server client