IP地址(公网IP),标定了主机的唯一性。
端口号,标识特定主机上的网络进程的唯一性。
因此IP+端口号port,就能标识全网唯一的一个进程。
IP+端口号 = 套接字。称之为套接字编程。
(利用套接字进行网络通信,本质上也是进程间通信的,只是这两个进程不在同一个主机上,要想进行通信,必须通过网络)
TCP:传输控制协议,传输层协议的一种。有链接。可靠传输。面向字节流。
UDP:用户数据报协议,传输层协议的一种。无连接。不可靠传输。面向数据报。
udp_server.hpp
#ifndef _UDP_SERVER_HPP_
#define _UDP_SERVER_HPP_
#include "log.hpp"
#include
#include
#include
#include
#include
#include
#include
// 基于UDP协议的服务端
class UdpServer
{
public:
UdpServer(uint16_t port, std::string ip = "")
: _port(port), _ip(ip), _sock(-1)
{
}
void initServer()
{
// 1.创建套接字
_sock = socket(AF_INET, SOCK_DGRAM, 0); // 1.套接字类型:网络套接字(不同主机间通信) 2.面向数据报还是面向字节流:UDP面向数据报
// SOCK_DGRAM支持数据报(固定最大长度的无连接、不可靠消息)。
if (_sock < 0)
{
// 创建套接字失败?
logMessage(FATAL, "%d:%s", errno, strerror(errno));
exit(2);
}
// 2.bind : 将用户设置的ip和port在内核中和我们当前的进程强关联
struct sockaddr_in local; // 传给bind的第二个参数,存储ip和port的信息。
local.sin_family = AF_INET;
// 服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络!-> 故需要转换为网络字节序
local.sin_port = htons(_port); // host->network l 16
// "192.168.110.132" -> 点分十进制字符串风格的IP地址,每一个区域取值范围是[0-255]: 1字节 -> 4个区域,4字节
// INADDR_ANY:让服务器在工作过程中,可以从本机的任意IP中获取数据(一个服务器可能不止一个ip,(这块有些模糊)
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
// 点分十进制字符串风格的IP地址 <-> 4字节整数 4字节主机序列 <-> 网络序列 inet_addr可完成上述工作
if (bind(_sock, (struct sockaddr *)&local, sizeof local) < 0) // !!!
{
logMessage(FATAL, "bind : %d:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "init udp server %s...", strerror(errno));
}
void start()
{
// 作为一款网络服务器,永远不退出的!-> 服务器启动 -> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!
char buff_message[1024]; // 存储从client发来的数据
char back_message[124];
std::string back_str;
for (;;)
{
struct sockaddr_in peer; // 输出型参数
memset(&peer, 0, sizeof peer);
socklen_t len = sizeof peer; // 输出+输入型参数
// 读取client发来的数据
ssize_t sz = recvfrom(_sock, buff_message, sizeof(buff_message) - 1, 0, (struct sockaddr *)&peer, &len); // 哪个ip/port给你发的
// receive a message from a socket,从一个套接字(或许对应网卡)中接收信息
if (sz > 0)
{
// 从client获取到了数据
buff_message[sz] = 0;
// 你发过来的字符串是指令 ls -a -l, rm -rf ~
if(strcasestr(buff_message, "rm") != nullptr)
{
std::string err_msg = "可恶..";
std::cout << inet_ntoa(peer.sin_addr)<< " : "<< ntohs(peer.sin_port) << err_msg << buff_message << std::endl;
sendto(_sock, err_msg.c_str(), err_msg.size(), 0, (struct sockaddr*)&peer, len);
continue;
}
FILE* fp = popen(buff_message, "r");
if(fp == nullptr)
{
logMessage(ERROR, "popen : %d:%s\n", errno, strerror(errno));
continue;
}
while(fgets(back_message, sizeof(back_message), fp) != nullptr)
{
back_str += back_message;
}
fclose(fp);
}
else
{
// back_str.clear();
}
// 作为一款伪shell server,任务就是写回client发来的命令的结果,结果存储在back_str中
sendto(_sock, back_str.c_str(), back_str.size(), 0, (struct sockaddr *)&peer, len);
back_str.clear();
}
}
void start1()
{
// 作为一款网络服务器,永远不退出的!-> 服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!
char buff_message[1024]; // 存储从client发来的数据
for (;;)
{
struct sockaddr_in peer; // 输出型参数
memset(&peer, 0, sizeof peer);
socklen_t len = sizeof peer; // 输出+输入型参数
// 读取client发来的数据
ssize_t sz = recvfrom(_sock, buff_message, sizeof(buff_message) - 1, 0, (struct sockaddr *)&peer, &len); // 哪个ip/port给你发的
// receive a message from a socket,从一个套接字(或许对应网卡)中接收信息
if (sz > 0)
{
// 从client获取到了非空数据
buff_message[sz] = 0;
uint16_t cli_port = ntohs(peer.sin_port);
std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节网络序列ip->字符串风格的IP
printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buff_message); // 可有可无...服务端显示一下客户端发来了什么
}
else
{
buff_message[0] = 0;
}
// 作为一款echo server,任务就是写回client发来的数据
sendto(_sock, buff_message, strlen(buff_message), 0, (struct sockaddr *)&peer, len);
}
}
~UdpServer()
{
close(_sock);
}
private:
// 一个服务器,一般必须需要ip地址和port(16位的整数)
uint16_t _port; // 端口号
std::string _ip; // ip
int _sock; // 套接字
};
#endif
udp_server.cc
#include "udp_server.hpp"
#include
#include
#include
static void Usage(const char *proc)
{
std::cout << "\nUsage: " << proc << " port\n"
<< std::endl;
}
// 格式:./udp_server 8080
// 疑问: 为什么不需要传ip?
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);
std::unique_ptr svr(new UdpServer(port));
svr->initServer();
svr->start();
return 0;
}
udp_client.cc
#include
#include
#include
#include
#include
#include
#include
#include
static void Usage(char *proc)
{
std::cout << "\nUsage : " << proc << " server_ip server_port\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); // 网络套接字,udp面向数据报
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_port = htons(atoi(argv[2]));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(argv[1]);
char message_svr[1024];
memset(message_svr, 0, sizeof message_svr);
while (true)
{
std::cout << "client# ";
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); // server:给哪个ip/port发
struct sockaddr_in temp;
memset(&temp, 0, sizeof temp);
socklen_t len = sizeof temp;
ssize_t sz = recvfrom(sock, message_svr, sizeof message_svr - 1, 0, (struct sockaddr *)&temp, &len); // temp:谁发给你的(哪个ip/port)
if(sz > 0)
{
message_svr[sz] = 0;
std::cout << "server echo# "<< message_svr << std::endl;
}
}
close(sock);
return 0;
}
log.hpp略了。
UDP是面向数据报的传输层协议。
服务端基本套路就是,socket创建套接字。bind绑定端口和ip。recvfrom获取客户端发来的数据,进行业务处理(根据这个服务器的类型),sendto发送给客户端。
客户端:socket,不需要显式地bind(见注释),sendto给服务器,recvfrom获取服务器给你的回应数据。
套接字有三种,域间套接字,原始套接字,网络套接字。域间用于同一个主机内不同进程通信。原始略了。网络用于不同主机间进程进行数据通信。
这是三个场景,应当对应三套接口。但是不想设计过多接口,因此对于bind,recvfrom,sendto都有一个struct sockaddr*类型的参数。如果想使用网络套接字进行网络通信,就传struct sockaddr_in*类型然后强转。如果想使用域间套接字,就传struct sockaddr_un*类型。达到了一套接口,根据实参类型不同对应不同功能的目的。
上方示例其实就是一个udp套接字编程的基本使用,服务器也是一个最简单的不能再简单的echo server。