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->字符串风格的IPprintf("[%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; // ipint _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和PORTsendto(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。
上一篇:PV,VG,LV
下一篇:python第五天作业~基础练习