Linux-UDP套接字编程
一.认识IP地址
IP 协议有两个版本, IPv4 和 IPv6. 我们之后凡是提到 IP 协议, 没有特殊说明的,默认都是指 IPv4。
- IP 地址是在 IP 协议中, 用来标识网络中不同主机的地址;
- 对于 IPv4 来说, IP 地址是一个 4 字节, 32 位的整数;
- 我们通常也使用 "点分十进制" 的字符串表示 IP 地址, 例如 192.168.0.1 ; 用点分割的每一个数字表示一个字节, 范围是 0 - 255;
跨网段的主机的数据传输. 数据从一台计算机到另一台计算机传输过程中要经过一个或多个路由器:
为什么去目标主机要经过路由器呢?同时目的IP又有什么用呢?
上图展示了源主机发送数据到目标主机的过程。我们发现,比如说有一个人今天要从广州到北京。从广州开始出发,他的下一站假设是湖南,此时湖南就是他当前要去往的地方,相当于mac地址。最终的目的地是北京,其实就相当于IP地址。
我们再结合这个流程重新认识下数据的封装和解包过程:
TIPS:为什么需要使用IP地址?
我们之前也已经提到过,mac地址本就可以标识主机的唯一性,为什么还需要使用IP地址呢。这里我们从五个方面说明:
1.MAC地址虽然全球唯一,但缺乏位置信息
MAC地址是设备出厂时烧录的物理标识,虽然能确保全球唯一性,但它只是单纯的编号,没有任何地理位置或网络拓扑信息。如果只用MAC地址通信,数据包无法高效路由,因为网络设备无法判断目标主机在哪个方向,只能盲目广播,导致网络拥塞。
2.IP地址提供分层结构,支持高效路由
IP地址的设计包含网络号和主机号,形成层次化结构。路由器可以根据IP地址的网段快速判断数据包的转发方向,就像快递系统根据省、市、区逐级递送。这种分层寻址让互联网的规模扩展成为可能,而MAC地址的扁平结构无法做到这一点。
3.MAC地址只在局域网内有效
MAC地址的作用范围局限于本地网络(如家庭Wi-Fi或公司内网),交换机依靠它进行精确的端口转发。但跨网络通信时,IP地址才是核心,路由器不关心目标的MAC地址,只根据IP地址选择最佳路径。
4.IP地址能屏蔽底层差异
不同的局域网可能使用不同的传输技术(如以太网、Wi-Fi、光纤等),IP地址作为统一逻辑地址,能隐藏底层细节,让上层应用无需关心具体网络环境。而MAC地址与硬件绑定,无法提供这种抽象能力。
5.目的IP决定路径,MAC地址决定最后一跳
数据包在传输过程中,每经过一个路由器,其MAC地址都会变化(更新为下一跳的地址),但IP地址始终不变。IP负责宏观路径选择,MAC地址只负责当前局域网的精准送达,二者各司其职才能实现高效通信。
就好比为什么人们本来就有身份证可以标识自身的唯一性。为什么点外卖时需要提供手机号和个人位置信息。为什么在公司和学校需要有个人对应的工号学号。所以我们总是在理解计算机中的概念时往往会借助现实生活中的例子,因为人类世界与计算机世界总是息息相关的,计算机本为人的产物。
二.网络套接字编程预备
2.1理解目的IP地址和源IP地址
IP在网络中标识主机的唯一性,但是IP也是有分类的,后面我们会介绍IP 的分类, 以及会详细阐述 IP 的特点。
眼下还有一个重要的问题,数据通过了IP地址传到了目标主机,这些数据传过来干什么?让用户使用!也就是说,数据传输到主机是目的吗? 不是的。 因为数据是给人用的。
那么怎么让用户使用这些数据呢?比如,怎么看到聊天信息的呢? 怎么执行下载任务呢? 怎么浏览网页信息呢? 通过启动的qq, 迅雷, 浏览器。我们发现,qq, 迅雷, 浏览器不就是用户所启动的进程吗,换句话说, 进程是人在系统中的代表, 只要把数据给进程, 人就相当于就拿到了数据。
所以: 数据传输到主机不是目的, 而是手段。 到达主机内部, 在交给主机内的进程,才是目的。那么怎么标识进程的唯一性呢?当然是pid了?错!端口号。
2.2认识端口号
不要着急,我们先来认识端口号,在解释为什么不用pid。端口号(port)是传输层协议的内容:
- 端口号是一个 2 字节 16 位的整数
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理
- IP 地址 + 端口号能够标识网络上的某一台主机的某一个进程
- 一个端口号只能被一个进程占用
2.2.1端口号的划分
-
0 - 1023: 知名端口号, HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的端口号都是固定的(除非我们调用管理员权限才可以绑定使用)。
-
1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的。
2.2.2端口号与进程ID
为什么不使用进程ID,本质其实是为了解耦。进程 ID 属于系统概念, 技术上也具有唯一性, 确实可以用来标识唯一的一个进程, 但是这样做, 会让系统进程管理和网络强耦合, 实际设计的时候, 并没有选择这样做。
另外, 一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定。可以把端口号想象成公司里的分机号,而进程就像接听电话的员工。一个员工可以负责多个分机号(比如客服小王同时管理投诉热线8080和售后咨询9090),这就好比一个进程(如Web服务器)可以绑定监听多个端口(如80和443)。但一个分机号不能同时分配给多个员工(比如8080不能既归小王又归小李),否则来电时系统会不知道转给谁——同理,操作系统不允许多个进程同时绑定同一个端口,否则数据包来了无法确定交给哪个程序处理。
不过也有例外,像SO_REUSEPORT这种特殊技术,就像让一个团队共享同一个分机号,但需要系统自动分配来电来避免冲突。这种情况我们一般不考虑。
TIPS:源端口号和目的端口号
传输层协议(TCP 和 UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号。就是在描述 "数据是谁发的, 要发给谁"。
2.3认识套接字(Socket) 及传输层的典型代表(TCP/UDP)
2.3.1什么是套接字
综上, IP 地址用来标识互联网中唯一的一台主机, port 用来标识该主机上唯一的一个网络进程。IP+Port 就能表示互联网中唯一的一个进程。
所以, 通信的时候, 本质是两个互联网进程代表人来进行通信, {srcIp,srcPort, dstIp, dstPort}这样的 4 元组就能标识互联网中唯二的两个进程。我们发现,网络通信的本质,其实就是进程间通信!
我们把 ip+port 叫做套接字 socket。
2.3.2 认识TCP/UDP
如果我们了解了系统, 也了解了网络协议栈, 我们就会清楚, 传输层是属于内核的, 那么我们要通过网络协议栈进行通信, 必定调用的是传输层提供的系统调用, 来进行的网络通信。
2.3.3网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
- TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个 TCP/IP 规定的网络字节序来发送/接收数据
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
为使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
- h 表示 host(主机),n 表示 network(网络),l 表示 32 位长整数,s 表示 16 位短整数
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
例如 htonl 表示将 32 位的长整数从主机字节序转换为网络字节序,例如将 IP 地址转换后准备发送。
2.3.4Socket编程接口
1.Socket常用API
// 创建通信端点
int socket(int domain, int type, int protocol);
/* domain: 协议族(AF_INET/IPv4, AF_INET6/IPv6等)* type: 套接字类型(SOCK_STREAM/TCP, SOCK_DGRAM/UDP)* protocol: 通常填0(自动选择)*/// 绑定地址和端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/* sockfd: socket描述符* addr: 指向sockaddr结构体的指针(需转换为sockaddr_in等具体类型)* addrlen: 地址结构体长度*/// 设置监听队列
int listen(int sockfd, int backlog);
/* sockfd: 已绑定的socket描述符* backlog: 最大挂起连接数(典型值5-10)*/// 接受连接请求
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/* sockfd: 处于监听状态的socket* addr: 用于存储客户端地址(可NULL)* addrlen: 输入输出参数(需先初始化)*/// 发起连接请求
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/* sockfd: 客户端socket描述符* addr: 服务器地址结构体* addrlen: 地址结构体长度*/
2.SocketAddr结构
socket API 是一层抽象的网络编程接口,适用于各种底层网络协议,如 IPv4、 IPv6,以及后面要讲的 UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同
- IPv4 和 IPv6 的地址格式定义在 netinet/in.h 中,IPv4 地址用 sockaddr_in 结构体表示,包括 16 位地址类型, 16 位端口号和 32 位 IP 地址。
- IPv4、 IPv6 地址类型分别定义为常数 AF_INET、 AF_INET6. 这样,只要取得某种 sockaddr 结构体的首地址,不需要知道具体是哪种类型的 sockaddr 结构体,就可以根据地址类型字段确定结构体中的内容(其实就是c版本的多态啦)。
- socket API 可以都用 struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr, 这样的好处是程序的通用性, 可以接收 IPv4, IPv6, 以及 UNIX DomainSocket 各种类型的 sockaddr 结构体指针做为参数。
我们再来结合源码简化的看下sockaddr的结构:
#include <sys/socket.h>struct sockaddr {sa_family_t sa_family; // 地址族(如 AF_INET、AF_INET6)char sa_data[14]; // 地址数据(实际存储 IP 和端口)
};
由于我们一般使用的是sockaddr_in结构,所以我们详细介绍它:
#include <netinet/in.h>//IPv4
struct sockaddr_in {sa_family_t sin_family; // 地址族(必须为 AF_INET/PF_INET)in_port_t sin_port; // 16 位端口号(需用 htons() 转换字节序)struct in_addr sin_addr; // 32 位 IPv4 地址char sin_zero[8];// 填充字段(未使用,通常置0)
};struct in_addr {uint32_t s_addr; // 网络字节序的 IPv4 地址(需用 inet_pton() 转换)
};//IPv6(了解即可,虽然比4好,但不是目前重点)
struct sockaddr_in6 {sa_family_t sin6_family; // AF_INET6in_port_t sin6_port; // 端口号uint32_t sin6_flowinfo; // 流信息struct in6_addr sin6_addr; // 128 位 IPv6 地址uint32_t sin6_scope_id; // 作用域 ID
};
对于结构体in_addr:
#include <netinet/in.h> // 定义位置struct in_addr {uint32_t s_addr; // 32位网络字节序的IPv4地址
};
专门用于存储 网络字节序(大端) 的IPv4地址(例如 0x7F000001
表示 127.0.0.1
)。
对于in_port_t类型其实就是uint16_t类型:
/* Type to represent a port. */
typedef uint16_t in_port_t;
sa_family_t:
/* POSIX.1g specifies this type name for the `sa_family' member. */
typedef unsigned short int sa_family_t;
其实本质上就是一个无符号32位整型。它的地址簇:
/* Protocol families. */
#define PF_UNSPEC 0 /* Unspecified. */
#define PF_LOCAL 1 /* Local to host (pipes and file-domain). */
#define PF_UNIX PF_LOCAL /* POSIX name for PF_LOCAL. */
#define PF_FILE PF_LOCAL /* Another non-standard name for PF_LOCAL. */
#define PF_INET 2 /* IP protocol family. */
#define PF_AX25 3 /* Amateur Radio AX.25. */
#define PF_IPX 4 /* Novell Internet Protocol. */
#define PF_APPLETALK 5 /* Appletalk DDP. */
#define PF_NETROM 6 /* Amateur radio NetROM. */
#define PF_BRIDGE 7 /* Multiprotocol bridge. */
#define PF_ATMPVC 8 /* ATM PVCs. */
#define PF_X25 9 /* Reserved for X.25 project. */
#define PF_INET6 10 /* IP version 6. */
#define PF_ROSE 11 /* Amateur Radio X.25 PLP. */
#define PF_DECnet 12 /* Reserved for DECnet project. */
#define PF_NETBEUI 13 /* Reserved for 802.2LLC project. */
#define PF_SECURITY 14 /* Security callback pseudo AF. */
#define PF_KEY 15 /* PF_KEY key management API. */
#define PF_NETLINK 16
#define PF_ROUTE PF_NETLINK /* Alias to emulate 4.4BSD. */
#define PF_PACKET 17 /* Packet family. */
#define PF_ASH 18 /* Ash. */
#define PF_ECONET 19 /* Acorn Econet. */
#define PF_ATMSVC 20 /* ATM SVCs. */
#define PF_RDS 21 /* RDS sockets. */
#define PF_SNA 22 /* Linux SNA Project */
#define PF_IRDA 23 /* IRDA sockets. */
#define PF_PPPOX 24 /* PPPoX sockets. */
#define PF_WANPIPE 25 /* Wanpipe API sockets. */
#define PF_LLC 26 /* Linux LLC. */
#define PF_IB 27 /* Native InfiniBand address. */
#define PF_MPLS 28 /* MPLS. */
#define PF_CAN 29 /* Controller Area Network. */
#define PF_TIPC 30 /* TIPC sockets. */
#define PF_BLUETOOTH 31 /* Bluetooth sockets. */
#define PF_IUCV 32 /* IUCV sockets. */
#define PF_RXRPC 33 /* RxRPC sockets. */
#define PF_ISDN 34 /* mISDN sockets. */
#define PF_PHONET 35 /* Phonet sockets. */
#define PF_IEEE802154 36 /* IEEE 802.15.4 sockets. */
#define PF_CAIF 37 /* CAIF sockets. */
#define PF_ALG 38 /* Algorithm sockets. */
#define PF_NFC 39 /* NFC sockets. */
#define PF_VSOCK 40 /* vSockets. */
#define PF_KCM 41 /* Kernel Connection Multiplexor. */
#define PF_QIPCRTR 42 /* Qualcomm IPC Router. */
#define PF_SMC 43 /* SMC sockets. */
#define PF_XDP 44 /* XDP sockets. */
#define PF_MCTP 45 /* Management component transport protocol. */
#define PF_MAX 46 /* For now.. *//* Address families. */
#define AF_UNSPEC PF_UNSPEC
#define AF_LOCAL PF_LOCAL
#define AF_UNIX PF_UNIX
#define AF_FILE PF_FILE
#define AF_INET PF_INET
#define AF_AX25 PF_AX25
#define AF_IPX PF_IPX
#define AF_APPLETALK PF_APPLETALK
#define AF_NETROM PF_NETROM
#define AF_BRIDGE PF_BRIDGE
#define AF_ATMPVC PF_ATMPVC
#define AF_X25 PF_X25
#define AF_INET6 PF_INET6
#define AF_ROSE PF_ROSE
#define AF_DECnet PF_DECnet
#define AF_NETBEUI PF_NETBEUI
#define AF_SECURITY PF_SECURITY
#define AF_KEY PF_KEY
#define AF_NETLINK PF_NETLINK
#define AF_ROUTE PF_ROUTE
#define AF_PACKET PF_PACKET
#define AF_ASH PF_ASH
#define AF_ECONET PF_ECONET
#define AF_ATMSVC PF_ATMSVC
#define AF_RDS PF_RDS
#define AF_SNA PF_SNA
#define AF_IRDA PF_IRDA
#define AF_PPPOX PF_PPPOX
#define AF_WANPIPE PF_WANPIPE
#define AF_LLC PF_LLC
#define AF_IB PF_IB
#define AF_MPLS PF_MPLS
#define AF_CAN PF_CAN
#define AF_TIPC PF_TIPC
#define AF_BLUETOOTH PF_BLUETOOTH
#define AF_IUCV PF_IUCV
#define AF_RXRPC PF_RXRPC
#define AF_ISDN PF_ISDN
#define AF_PHONET PF_PHONET
#define AF_IEEE802154 PF_IEEE802154
#define AF_CAIF PF_CAIF
#define AF_ALG PF_ALG
#define AF_NFC PF_NFC
#define AF_VSOCK PF_VSOCK
#define AF_KCM PF_KCM
#define AF_QIPCRTR PF_QIPCRTR
#define AF_SMC PF_SMC
#define AF_XDP PF_XDP
#define AF_MCTP PF_MCTP
#define AF_MAX PF_MAX
我们可以看到AF与PF是一样的,底层其实都是对整数的重定义。
三.使用UDP实现简单的网络服务
3.1(IP)地址转换函数
通常用点分十进制的字符串表示 IP 地址,以下函数可以在字符串表示 和in_addr 表示之间转换;
inet_aton / inet_ntoa (IPv4):
#include <arpa/inet.h>int inet_aton(const char *cp, struct in_addr *inp); // 字符串转二进制
char *inet_ntoa(struct in_addr in); // 二进制转字符串
in_addr_t inet_addr(const char *cp); //inet_addr()本身是线程安全的,因为它不依赖静态缓冲区,当 //输入无效时会返回INADDR_NONE,但要注意"255.255.255.255"也会返回-1(即INADDR_NONE)
对于inet_ntoa,由于该函数转换完后字符串存储于静态存储区。所以当多次调用时会覆盖前一次的IP字符串。所以我们一般不推荐使用上面的inet_aton与inet_ntoa组合地址转换函数,我们更推荐使用下面的组合:
inet_pton / inet_ntop (同时支持IPv4和IPv6):
#include <arpa/inet.h>int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
即确保线程安全的同时又兼顾IPV4与IPV6地址。(或者和inet_addr混着使用,但还是成对的使用看着优雅舒服)。
3.2UdpEchoserver
在写通信代码之前,我们需要明确几个问题,先来看udp套接字相关的核心函数:bind,recvfrom与sendto:(socket本质是文件,所以通信之前需要给文件绑定通信目标信息,通信时需要传递绑定好目标信息的socket以给函数告诉其目标信息)
bind:
#include <sys/types.h>
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
recvfrom:
#include <sys/types.h>
#include <sys/socket.h>ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
sendto:
#include <sys/types.h>
#include <sys/socket.h>ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
而我们在写通信代码时,通常是有两个端的,一个服务端一个客户端。那么对于服务端,由于未来访问其的客户端不只一个,且各自的IP地址均不相同。所以我们通常让服务端的socket绑定的IP地址是0.0.0.0,即INADDR_ANY,同时绑定固定的端口号,表示但凡访问当前主机的客户端,只要它要访问的端口号是我所绑定的那个端口号,我一律接受并尝试处理请求。
对于客户端,我们的端口号也需要绑定,但不需要绑定固定的端口号,因为我自己知道就行不需要别人去使用。所以我们通常是直接不显示绑定,直接传一个未绑定的socket即可,此时sendto会自动帮助客户端socket绑定一个随机的端口号和当前客户端主机IP地址。
因此我们在执行客户端和服务端各自的可执行文件时,前者需要在后面加上服务端的IP地址和端口号,后者则需要加一个端口号(表示服务端提供服务的固定端口号)。
3.2.1Echoserver代码
这里会直接使用之前写的log.hpp与mutex.hpp,我们这里不再展示直接使用:
udpserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include "log.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>const int default_socket = -1;
using namespace LogModule;using func_t = std::function<void(std::string,sockaddr_in)>;class UdpServer
{
public:UdpServer(uint16_t port,func_t func):_socket(default_socket),_port(port),_isrunning(false),_func(func){}void Init(){//1.创建网络套接字_socket = socket(AF_INET,SOCK_DGRAM,0);if(_socket == -1){LOG(LogLevel::FATAL) << "create socket error";exit(1);}LOG(LogLevel::INFO) << "create socket success,socket:" << _socket;//2.绑定对应端口号(IP地址不显示绑定,表示所有传给端口port的消息全部接受)struct sockaddr_in server_addr;bzero(&server_addr,sizeof(server_addr));server_addr.sin_family = PF_INET;server_addr.sin_port = htons(_port);server_addr.sin_addr.s_addr = INADDR_ANY;int n = bind(_socket,(struct sockaddr*)&server_addr,sizeof(server_addr));if(n == -1){LOG(LogLevel::FATAL) << "bind error!" << _socket;exit(2);}LOG(LogLevel::INFO) << "bind success!" << _socket;}void Start(){_isrunning = true;while(_isrunning){//接受消息char buffer[1024];memset(buffer,0,sizeof(buffer));struct sockaddr_in src_addr;socklen_t src_len = sizeof(src_addr);ssize_t n = recvfrom(_socket,buffer,sizeof(buffer),0,(struct sockaddr*)&src_addr,&src_len);if(n == -1){std::cout << "recvfrom error" << std::endl;exit(3);}std::cout << "[" << inet_ntoa(src_addr.sin_addr) << "]" << "[" << ntohs(src_addr.sin_port) << "]" << ":" << buffer << std::endl;//发送消息std::string echo_string = "server echo@ ";echo_string += buffer;n = sendto(_socket,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&src_addr,src_len);}}~UdpServer() {}
private:func_t _func;int _socket;//网络文件符uint16_t _port;//进程端口号(1024~65535)bool _isrunning;
};
udpserver.cc:
#include "UdpServer.hpp"
#include <memory>int main(int argc, char *argv[])
{//./udpserver portif(argc != 2){std::cerr << "Usage: " << argv[0] << " port" << std::endl;return 1;}uint16_t port = std::stoi(argv[1]);Output_To_Screen();std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);usvr->Init();usvr->Start();return 0;
}
udpclient.cc:
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int main(int argc, char *argv[])
{//./udpclient server_ip server_portif(argc != 3){std::cerr << "Usage: " << argv[0] << "server_ip" << "server_port" << std::endl;return 1;}std::string server_ip = argv[1];uint16_t server_port = std::atoi(argv[2]);//1.创建客户端套接字int c_socket = socket(AF_INET,SOCK_DGRAM,0);if(c_socket == -1){std::cout << "create socket error" << std::endl;exit(1);}std::cout << "create socket success,socket:" << c_socket << std::endl;//2.填写服务器信息struct sockaddr_in server_addr;bzero(&server_addr,sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(server_port);server_addr.sin_addr.s_addr = inet_addr(server_ip.c_str());//3.向服务器发送消息,同时接收服务器发回的消息while(true){std::string input;std::cout << "Please Enter# ";std::getline(std::cin, input);int n = sendto(c_socket, input.c_str(), input.size(), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));(void)n;char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);int n1 = recvfrom(c_socket, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);if(n1 > 0){buffer[n1] = 0;std::cout << buffer << std::endl;}}return 0;
}
3.2.2基于echoserver版本实现的简单聊天室
其实思路很简单,因为我们发消息的时候,比如qq或微信,通常发出的消息所有在线用户都可以看到,包括我们自己。所以我们只需要在服务端维护一张在线用户表,当有用户发消息时将该消息回显给所有的用户即可:
由于我们之后需要频繁使用网络地址和主机地址之间进行转换,所以我们这里实现一个类(InterAddr)方便我们之后直接调用:
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 网络地址和主机地址之间进行转换的类class InetAddr
{
public:InetAddr(struct sockaddr_in &addr) : _addr(addr){_port = ntohs(_addr.sin_port); _ip = inet_ntoa(_addr.sin_addr);}uint16_t Port() {return _port;}std::string Ip() {return _ip;}const struct sockaddr_in &NetAddr() { return _addr; }bool operator==(const InetAddr &addr){return addr._ip == _ip && addr._port == _port;}std::string StringAddr(){return _ip + ":" + std::to_string(_port);}~InetAddr(){}private:struct sockaddr_in _addr;std::string _ip;uint16_t _port;
};
(大家这里可以先使用下inet_ntoa,之后再换成inet_ptoa即可,博主这里就投个懒了)。
维护在线用户表:Route.hpp
#include "InterAddr.hpp"
#include <vector>
#include "log.hpp"class Route
{
private:bool IsOnline(InetAddr& peer){for(auto& i : _online_users){if(i == peer){return true;}}return false;}
public:Route() {}void MessageHandle(int socket,std::string& message,InetAddr& peer){if(!IsOnline(peer)){_online_users.emplace_back(peer);LOG(LogLevel::INFO) << "新增一个在线用户:" << "[" << peer.StringAddr() << "]";}std::string send_message = "[" + peer.StringAddr() + "]:" + message; //将当前用户所发消息发给所有用户(包括发消息的用户自己)for(InetAddr& i : _online_users){sendto(socket,send_message.c_str(),send_message.size(),0,(struct sockaddr*)&i,sizeof(i));}//默认用户在线if(message == "QUIT"){auto it = _online_users.begin();while(it != _online_users.end()){if(*it == peer){_online_users.erase(it);break;}}LOG(LogLevel::INFO) << "用户离线" << "[" << peer.StringAddr() << "]";}}~Route() {}
private:std::vector<InetAddr> _online_users;
};
udpserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include "log.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "InterAddr.hpp"const int default_socket = -1;
using namespace LogModule;using func_t = std::function<void(int,std::string,InetAddr& peer)>;class UdpServer
{
public:UdpServer(uint16_t port,func_t func):_socket(default_socket),_port(port),_isrunning(false),_func(func){}void Init(){//1.创建网络套接字_socket = socket(AF_INET,SOCK_DGRAM,0);if(_socket == -1){LOG(LogLevel::FATAL) << "create socket error";exit(1);}LOG(LogLevel::INFO) << "create socket success,socket:" << _socket;//2.绑定对应端口号(IP地址不显示绑定,表示所有传给端口port的消息全部接受)struct sockaddr_in server_addr;bzero(&server_addr,sizeof(server_addr));server_addr.sin_family = PF_INET;server_addr.sin_port = htons(_port);server_addr.sin_addr.s_addr = INADDR_ANY;int n = bind(_socket,(struct sockaddr*)&server_addr,sizeof(server_addr));if(n == -1){LOG(LogLevel::FATAL) << "bind error!" << _socket;exit(2);}LOG(LogLevel::INFO) << "bind success!" << _socket;}void Start(){_isrunning = true;while(_isrunning){//接受消息char buffer[1024];memset(buffer,0,sizeof(buffer));struct sockaddr_in src_addr;socklen_t src_len = sizeof(src_addr);ssize_t n = recvfrom(_socket,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&src_addr,&src_len);if(n == -1){std::cout << "recvfrom error" << std::endl;exit(3);}//使服务器仅接收数据,不发送数据buffer[n] = 0;//n为实际接收到的消息大小InetAddr client(src_addr);_func(_socket,buffer,client);// std::cout << "[" << inet_ntoa(src_addr.sin_addr) << "]" << "[" << ntohs(src_addr.sin_port) << "]" << ":" << buffer << std::endl;// //发送消息// std::string echo_string = "server echo@ ";// echo_string += buffer;// n = sendto(_socket,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&src_addr,src_len);}}~UdpServer() {}
private:func_t _func;int _socket;//网络文件符uint16_t _port;//进程端口号(1024~65535)bool _isrunning;
};
udpserver.cc
#include "UdpServer.hpp"
#include "ThreadPool.hpp"
#include <memory>
#include "Route.hpp"using task_t = std::function<void()>;int main(int argc, char *argv[])
{//./udpserver portif(argc != 2){std::cerr << "Usage: " << argv[0] << " port" << std::endl;return 1;}uint16_t port = std::stoi(argv[1]);Output_To_Screen();//1.路由功能,将用户所发消息发给用户自己和所有在线用户Route r;//2.创建线程池auto t = ThreadPool_Module::ThreadPool<task_t>::GetThreadPool();//3.网络服务std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port,[&r,&t](int socket,std::string Message,InetAddr& peer){task_t task = std::bind(&Route::MessageHandle,&r,socket,Message,peer);t->Enque(task);});usvr->Init();usvr->Start();return 0;
}
udpclient.cc
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Thread.hpp"using namespace My_Thread;std::string server_ip;
uint16_t server_port;
int c_socket;void Recv()
{while(true){char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);int n1 = recvfrom(c_socket, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);if(n1 > 0){buffer[n1] = 0;std::cerr << buffer << std::endl;}}
}void Send()
{//2.填写服务器信息struct sockaddr_in server_addr;bzero(&server_addr,sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(server_port);server_addr.sin_addr.s_addr = inet_addr(server_ip.c_str());while(true){std::string input;std::cout << "Please Enter# ";std::getline(std::cin, input);int n = sendto(c_socket, input.c_str(), input.size(), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));(void)n;if(input == "QUIT"){break;}}
}int main(int argc, char *argv[])
{//./udpclient server_ip server_portif(argc != 3){std::cerr << "Usage: " << argv[0] << "server_ip" << "server_port" << std::endl;return 1;}server_ip = argv[1];server_port = std::atoi(argv[2]);//1.创建客户端套接字c_socket = socket(AF_INET,SOCK_DGRAM,0);if(c_socket == -1){std::cout << "create socket error" << std::endl;exit(1);}std::cout << "create socket success,socket:" << c_socket << std::endl;//分别使用两个线程,一个接受消息,一个发送消息Thread rececer(Recv);Thread sender(Send);rececer.Start();sender.Start();rececer.join();sender.join();return 0;
}
上面的聊天室代码之前实现过的不再在此展现出来,需要的读者可移步线程池代码实现的文章。