文章目录
- Socket常见API
- 转网络字节序
- 网络数据传输的读
- 网络数据传输的写
- 简单的UDP网络程序服务端
- 基本结构
- Init() 服务端的初始化
- Run() 服务端的运行
- 服务端启动及测试
- 简单的UDP网络程序客户端
- 服务端客户端相互通信测试
- 服务端通过传入命令处理实现远程命令执行
- 参考代码
Socket常见API
-
创建
socket
文件描述符 (TCP/UDP
,客户端 + 服务端)NAME socket - create an endpoint for communication SYNOPSIS #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol); RETURN VALUE On success, a file descriptor for the new socket is returned. On error, -1 is returned, and errno is set appropriately.
该函数用于创建一个新的套接字以便进行网络通信的基础函数;
在网络编程中套接字(
socket
)是一个端点,它支持在不同计算机之间传输数据;其参数如下:
-
int domain
该参数指定协议簇,即套接字的地址族,常用的值有:
-
AF_INET
表示使用
IPv4
网络协议; -
AF_INET6
表示使用
IPv6
网络协议; -
AF_UNIX
表示使用域间套接字(本地通信协议,也叫UNIX域socket);
-
AF_PACKET
表示使用底层接口(用于直接访问网络设备,通常用于编写一些网络工具,例如抓包工具等);
-
-
int type
该参数为指定的套接字类型,常用的值包括:
-
SOCK_STREAM
流式套接字,提供面向连接,可靠的数据传输(如
TCP
); -
SOCK_DGRAM
数据报套接字,提供无连接的,不保证可靠的传输(如
UDP
); -
SOCK_RAM
原始套接字,提供对底层协议的直接访问;
-
-
int protocol
该参数为特定于协议簇
domain
的协议,设置为0
时系统将会自动选择合适的协议,常用值包括:-
IPPROTO_TCP
TCP
协议; -
IPPROTO_UDP
UDP
协议;
-
该函数调用失败时返回
-1
并设置全局变量errno
来指示错误类型,调用成功时则返回一个新的套接字文件描述符,是一个非负整数;返回一个套接字文件描述符本质为在Linux中具有 “一切皆文件” 的哲学;
该函数的常见错误码如下:
-
EACCES
权限被拒绝;
-
EAFNOSUPPORT
不支持指定的地址族;
-
EINVAL
无效参数;
-
EMFILE
超出了每个进程可打开的文件/套接字限制;
-
ENFILE
超出了系统范围内可打开的文件/套接字限制;
-
ENOMEM
或ENOBUFS
表示系统内存不足;
-
-
绑定端口号(
TCP/UDP
, 服务器)NAME bind - bind a name to a socket SYNOPSIS #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); RETURN VALUE On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
bind
函数用于将一个套接字与一个特定的地址(通常为本地地址)绑定在一起;绑定操作是将套接字与一个特定的
IP
地址和端口号关联的必要步骤;其参数如下:
-
int sockfd
该参数表示传入一个
socket
函数返回的一个套接字文件描述符,表示需要绑定的套接字; -
const struct sockaddr *addr
这个参数指向一个
sockaddr
结构的指针;这个结构包含了你要绑定的地址信息,具体的结构类型取决于使用的地址族;
-
AF_INET
地址族struct sockaddr_in { sa_family_t sin_family; // 地址族(应设置为 AF_INET) in_port_t sin_port; // 端口号(使用 htons 转换为网络字节序) struct in_addr sin_addr; // IP地址(使用 in_addr 结构表示) char sin_zero[8]; // 填充字段,通常设置为 0 }; struct in_addr { uint32_t s_addr; // 32位的IPv4地址(使用 htonl 转换为网络字节序) };
-
AF_UNIX
地址族struct sockaddr_un { sa_family_t sun_family; // 地址族(应设置为 AF_UNIX) char sun_path[108]; // 文件系统中的路径名,用作通信端点 };
在根据不同的地址族传入对应的结构体指针后需要对该传入的结构体指针进行强制类型转换为
struct sockaddr *
,否则函数调用时参数将不匹配;这样的实现可以看作是一种多态的实现方式,本质上
struct sockaddr
可以看作是一个基类,struct sockaddr_in
与struct sockaddr_un
看作是子类;基类接受子类的指针,根据不同类型子类的指针访问其对应成员;
-
-
socklen_t addrlen
该参数指定
addr
结构的长度(字节数),其中socklen_t
实际上是一个无符号整型unsigned_t
的typedef
;#define __U32_TYPE unsigned int __STD_TYPE __U32_TYPE __socklen_t; typedef __socklen_t socklen_t;
通常使用
sizeof(struct sockaddr_in)
或sizeof(struct sockaddr_in6)
;
该函数调用成功时返回
0
表示绑定成功,调用失败时返回-1
并设置全局变量errno
来指示错误类型;该函数调用时常见的错误码包括:
-
EACCES
表示尝试绑定到一个受保护的端口(通常为小于
1024
的端口,即知名端口号); -
EADDRINUSE
表示指定的地址已经在使用中;
-
EAADRNOTAVAIL
表示指定的地址不可用;
-
EBADF
表示
sockfd
不是有效的文件描述符; -
EINVAL
表示套接字已经成功被绑定;
-
ENOTSOCK
表示所传入的
sockfd
不是一个套接字文件描述符;
-
-
开始监听
socket
(TCP
,服务器)NAME listen - listen for connections on a socket SYNOPSIS #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog); RETURN VALUE On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
该函数用于在套接字上监听连接请求;
通常与服务端编程相关的使用场景种,该函数在创建和绑定一个套接字之后调用,为的是使该套接字接受来自客户端连接请求;
参数如下:
-
int sockfd
该参数表示传入一个由
socket
与bind
函数创建和绑定的套接字文件描述符,表示一个服务端的套接字; -
int backlog
该参数表示指定等待连接队列的最大长度,如果有更多的连接请求到来但队列已经满了,这些额外的连接请求将被拒绝或者忽略;
这是内核为尚未处理的连接建立一个等待队列,其值会影响网络性能和并发能力;
当函数调用成功时返回
0
表示监听成功;调用失败时返回
-1
并设置全局变量errno
来指示错误类型;其可能出现的错误码为如下:
-
EADDRINUSE
表示指定的地址已经被使用且该地址没有被关闭;
或是套接字已经被监听;
-
EBADF
表示
sockfd
不是有效的文件描述符; -
ENOTSOCK
表示
sockfd
不是一个套接字文件描述符; -
EOPNOTSUPP
表示该套接字不支持
listen
操作(例如原始套接字);
-
-
接受请求 (
TCP
, 服务器)NAME accept, accept4 - accept a connection on a socket SYNOPSIS #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); #define _GNU_SOURCE /* See feature_test_macros(7) */ #include <sys/socket.h> int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags); RETURN VALUE On success, these system calls return a nonnegative integer that is a descriptor for the accepted socket. On error, -1 is returned, and errno is set appropriately.
accept
函数和accept4
函数是用于从监听状态的套接字接受连接请求的系统调用;通常在调用
listen
函数后使用这两个函数来接受和处理客户端的连接请求;通常使用
accept
接口即可;参数如下:
-
int sockfd
由之前对
socket
,bind
和listen
函数的调用创建并配置好的套接字文件描述符;出去纳入的套接字处于监听状态;
-
struct sockaddr *addr
为一个输出型参数;
指向一个
struct sockaddr
结构体的指针;在
accept
成功返回时,这个结构体将包含连接上来的客户端地址信息,可传入nullptr
表示无需接受客户端地址信息; -
socklen_t addrlen
同样为输出型参数;
指向一个
socklen_t
类型的变量,用于存储addr
结构体的大小;调用返回时,这个变量将包含客户端地址信息的实际大小,传入
nullptr
表示无需接受客户端信息; -
flags
(仅accept4
)该参数为传递给
accept4
的额外标志,可用来设置一下选项:-
SOCK_NONBLOCK
使返回的文件描述符处于非阻塞模式;
-
SOCK_CLOEXEC
为返回的文件描述符设置
O_CLOEXEC
执行时关闭标志;
-
这个函数调用成功使返回一个非负整数,这个非负整数是新创建的已连接套接字的文件描述符,新描述符与请求连接的客户端通信;
调用失败时返回
-1
并设置全局变量errno
用于指示错误类型;常见的错误码为:
-
EAGAIN
或EWOULDBLOCK
表示套接字被标记为非阻塞模式,且没有挂起的连接;
-
EBADF
表示无效文件描述符;
-
ECONNABORTED
表示连接被终止;
-
EFAULT
表示指针参数无效;
-
EINTR
表示调用被信号中断;
-
EINVAL
表示套接字未处于监听状态;
-
EMFILE
表示进程文件描述符已满;
-
ENFILE
表示系统文件描述符已满;
-
ENOTSOCK
表示文件描述符不是套接字;
-
EOPNOTSUPP
表示套接字不支持接受(
accept
)操作;
-
-
建立连接 (
TCP
, 客户端)NAME connect - initiate a connection on a socket SYNOPSIS #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); RETURN VALUE If the connection or binding succeeds, zero is returned. On error, -1 is returned, and errno is set appropriately.
这个函数用于在套接字上发起到指定地址的连接,用于将客户端的套接字连接到远程服务器的地址和端口,是客户端编程中的关键一步;
其参数如下:
-
int sockfd
该参数表示传入一个套接字文件描述符;
这个套接字是之前调用
socket
函数创建的套接字; -
struct sockaddr *addr
指向一个
struct sockaddr
结构体的指针;该结构体包含了要连接的远程服务器的地址信息,对于
IPv4
地址,通常需要传入一个struct sockaddr_in
类型的指针并强制类型转换为struct sockaddr
指针类型;对于
IPv6
而言需要传入struct sockaddr_in6
,UNIX
域则传入struct sockaddr_un
,对应的也是要强制类型转换为struct sockaddr*
; -
socklen_t addrlen
表示传入一个
socklen_t
类型的变量作为指定addr
结构体的大小,其中单位是字节;
该函数调用成功时返回
0
表示连接操作成功;调用失败时则返回
-1
并设置全局变量errno
用于指示错误类型;常见的错误码为:
-
EACCES
或EPERM
表示权限问题导致连接被拒绝;
-
EADDRINUSE
表示本地地址已在使用中,且需要绑定该地址;
-
EAFNOSUPPORT
表示所提供的地址族在该套接字中不被支持;
-
EAGAIN
表示临时资源不可用;
-
EALREADY
表示套接字是非阻塞的,目前已有操作正在进行中;
-
EBADF
表示无效文件描述符;
-
ECONNEREFUSED
表示目标地址没有监听或主动拒绝连接;
-
EFAULT
表示指针参数无效;
-
EINPROGRESS
表示非阻塞套接字正在处理连接请求;
-
EINVAL
表示套接字已绑定到本地地址或者参数无效;
-
ENETURNREACH
表示网络无法到达目标地址;
-
ENOTSOCK
表示文件描述符不是套接字;
-
转网络字节序
数据在进行网络传输的时候需要转成标准的网络字节序,本质原因是在数据传输的过程中网络通信的两端的字节序要相同以确保数据发送至对端时出现数据的解析错误;
-
hton
系列函数hton
系列函数用于在网络编程中将主机字节序转换为网络字节序;确保数据在不同计算机体系结构之间传输时的一致性的重要步骤;
-
htons
函数#include <arpa/inet.h> uint16_t htons(uint16_t hostshort);
该函数用于将
16
位无符号短整数从主机字节序转换为网络字节序;返回值为转换后的网络字节序的
16
位无符号短整数; -
htonl
函数#include <arpa/inet.h> uint32_t htonl(uint32_t hostlong);
该函数用于将
32
位无符号长整形从主机字节序转换位网络字节序;返回值为转换后的网络字节序的
32
位无符号长整形;该函数常用于将
IP
地址转换为网络字节序,尤其是在直接处理IP
地址时,如设置套接字的IP
时;struct sockaddr_in server_addr; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 将本地地址转换为网络字节序 // 其中 INADDR_ANY 表示接收来自所有IP的数据
-
ntohs
函数#include <arpa/inet.h> uint16_t ntohs(uint16_t netshort);
该函数用于将
16
位无符号短整型从网络字节序转换位主机字节序;返回值为转换后的主机字节序的
16
位无符号短整型; -
ntohl
函数#include <arpa/inet.h> uint32_t ntohl(uint32_t netlong);
该函数用于将
32
位无符号长整型从网络字节序转换位主机字节序;返回值为转换后的主机字节序的
32
位无符号长整型;该函数通常用于接收到的数据,特别是当你从网络上接收到一个以网络字节序编码的
IP
地址时;uint32_t ip = ntohl(server_addr.sin_addr.s_addr); // 将网络字节序IP地址转换为主机字节序
-
-
inet
系列函数该系列函数主要用于在
IP
地址的字符串表示形式与其二进制表示形式之间进行转换;这些函数通常用于处理
IP
地址的格式转换和字节序的转换;-
inet_aton
函数int inet_aton(const char *cp, struct in_addr *inp);
该函数用于将点分十进制表示的
IPv4
地址转换为二进制形式,同时检测输入地址的有效性;参数
const char* cp
表示点分十进制的IPv4
地址字符串;参数
struct in_addr *inp
表示指向存储结果的struct in_addr
;该函数调用成功时返回非零值,调用失败时返回
0
,通常为输入地址格式无效; -
inet_addr
in_addr_t inet_addr(const char *cp);
该函数用于将点分十进制的
IPv4
地址转换为32
位网络字节序地址(二进制形式);参数
const char *cp
表示传入点分十进制形式IPv4
地址字符串;该函数调用成功时返回网络字节序的
32
位二进制表示;调用失败时返回
INADDR_NONE
(0xFFFFFFFF
),通常位输入地址格式无效; -
inet_ntoa
char *inet_ntoa(struct in_addr in);
该函数用于将二进制形式(
32
位网络字节序)的IPv4
地址转换为点分十进制字符串表示;参数
in
表示网络字节序的IPv4
地址结构;返回值为一个指向静态缓冲区的字符串指针,包含点分十进制的
IPv4
地址; -
其他函数
-
inet_network
该函数主要用于返回给定
IP
地址网络部分(被废弃); -
inet_makeaddr
构造一个
IP
地址,通过结合网络号和主机号(用于一些特定场合); -
inet_lnaof
/inet_netof
分别提取本地网络地址和网络字段,从实际
IPv4
地址中提取字部分;
这些函数是底层操作的一部分,通常比较少直接使用;
-
-
网络数据传输的读
通常recv
系列函数用于从套接字接收数据,用于读取从远程主机发送的数据;
-
recv
函数#include <sys/types.h> #include <sys/socket.h> ssize_t recv(int sockfd, void *buf, size_t len, int flags);
该函数用于从套接字中接收数据并将其存储在特定的缓冲区中,是一种最基本的从连接套接字(如
TCP
连接)接收数据的方法,适用于接收来自连接的连续字节流;参数如下:
-
int sockfd
该参数表示套接字的文件描述符,从中接收数据;
-
void *buf
指向要接收数据的缓冲区指针;
-
size_t len
该参数表示缓冲区的长度,即最多可以接收的数据量;
-
int flags
该参数指定接收操作的行为,常见标志包括:
-
MSG_WAITALL
表示等待接收到所有请求的数据;
-
MSG_DONTWAIT
表示非阻塞式接收数据;
-
MSG_PEEK
表示查看数据但不移出队列(这里的队列指用于暂时存储通过网络接口接收到的数据的缓冲区);
-
该函数调用成功时将返回接收到的数据的字节数,如果连接关闭则返回
0
,如果发生错误时则返回-1
并设置errno
全局变量来指示错误类型; -
-
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);
该函数用于从套接字中接收数据,并获取发送方的地址信息(通常用于
UDP
和其他无连接协议),在接收数据时同时获取对方发送的地址信息,这对于服务端处理多个客户端时非常有用;参数如下:
-
int sockfd
表示套接字文件描述符,从中接收数据;
-
void *buf
指向要接收数据的缓冲区指针;
-
size_t len
表示缓冲区的长度,即最多可以接收的数据量;
-
int flags
指定接收操作的行为,与上文的
recv
函数相同; -
struct sockaddr *src_addr
指向存储发送方地址的结构体指针,如果不关心发送方的地址可以传入
nullptr
; -
socklen_t *addrlen
指向存储
src_addr
大小的变量指针;在调用后,它将包含实际地址的大小;
当函数调用成功时返回接收到的字节数,如果连接关闭则返回
0
,发生错误时返回-1
并设置全局变量errno
来指示错误类型; -
-
recvmsg
函数#include <sys/types.h> #include <sys/socket.h> ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
该函数用于从套接字中接收数据并存储在
msghdr
结构中,该函数允许更复杂的接收操作,包括接收多个缓冲区的数据,辅助数据(文件描述符等)等;参数如下:
-
int sockfd
表示套接字文件描述符,从中接收数据;
-
struct msghdr *msg
指向
msghdr
结构体的指针,描述了接收缓冲区及相关信息;struct msghdr { void *msg_name; /* Optional address */ socklen_t msg_namelen; /* Size of address */ struct iovec *msg_iov; /* Scatter/gather array */ int msg_iovlen; /* # elements in msg_iov */ void *msg_control; /* Ancillary data, see below */ socklen_t msg_controllen; /* Ancillary data buffer len */ int msg_flags; /* Flags (unused with sendmsg) */ };
-
int flags
该参数指定接受操作的行为,与
recv
函数相同;
该函数调用成功时返回接收到的字节数,如果连接关闭则返回
0
,发生错误时返回-1
并设置全局变量errno
用于指示错误类型; -
网络数据传输的写
通常send
系列函数用于写入数据到套接字中;
-
send
函数#include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t len, int flags);
该函数用于向套接字发送数据,将缓冲区中的数据发送到已连接的套接字中,用于向
TCP
连接发送数据,是最常用的发送函数之一;参数如下:
-
int sockfd
套接字文件描述符,表示目标连接;
-
const void *buf
指向包含待发送数据缓冲区的指针;
-
size_t len
缓冲区中待发送数据的长度;
-
int flags
指定发送操作的行为,常用标志包括:
-
MSG_DONTWAIT
表示非阻塞式发送数据;
-
MSG_NOSIGNAL
表示不产生
SIGPIPE
信号;
-
-
-
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);
用于发送数据到指定的地址,即使为建立连接,主要用于无连接协议(如
UDP
),在未经建立连接的情况下直接将数据包发送到目标地址;参数如下:
-
int sockfd
套接字文件描述符;
-
const void *buf
指向包含待发送数据缓冲区的指针;
-
size_t len
缓冲区中待发送数据的长度;
-
int flags
指定发送操作行为(同上);
-
const struct sockaddr *dest_addr
指向包含目标地址的结构体指针;
-
socklen_t addrlen
目标地址结构体的长度;
函数调用成功是返回实际发送的字节数;
发生错误时返回
-1
,并设置全局变量errno
来指示错误类型; -
-
sendmsg
函数#include <sys/types.h> #include <sys/socket.h> ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
使用消息结构体
msghdr
向套接字发送数据,支持高级功能如多缓冲区和辅助数据(如文件描述符),其结构如上(recvmsg
函数中对该结构体有解释);参数如下:
-
int sockfd
套接字文件描述符;
-
const struct msghdr *msg
指向包含待发送信息的
msghdr
结构的指针; -
int flags
指定发送操作的行为(同上);
该函数调用成功时返回实际发送的字节数,发生错误时返回
-1
并设置全局变量errno
来指示错误类型; -
简单的UDP网络程序服务端
基本结构
/*
UdpServer.hpp 文件
用于实现服务端
*/
#ifndef UDPSERVER_HPP
#define UDPSERVER_HPP
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <cstring>
#include <iostream>
#include <memory>
#include <string>
#include "log.hpp" // 引用日志插件
#define BUF_SIZE 1024
Log log_; // 实例化了一个日志对象
enum { SOCK_CREATE_FAIL = 1, SOCK_BIND_FAIL };
class UdpServer {
public:
// 构造函数初始化成员变量
UdpServer(const uint16_t &port = defaultport,
const std::string &ip = defaultip)
: port_(port), ip_(ip), isrunning_(false) {}
~UdpServer() {
if(sockfd_>0)
close(sockfd_); // 关闭套接字文件描述符
}
void Init() {
// 用于实现服务端的初始化
/*
1. 创建UDP socket
2. bind 绑定
*/
}
void Run() {
// 用于服务端的运行
/*
服务端需要一直处于运行状态
1. 接收客户端传入的套接字信息
2. 处理用户所传入的数据
*/
}
private:
int sockfd_; // 套接字文件描述符
uint16_t port_; // 服务器进程端口号
std::string ip_; // IP 地址
bool isrunning_; // 表明服务器的运行状态
static const std::string defaultip; // 设置 ip 初始值
static const uint16_t defaultport; // 设置端口初始值
};
/*
为静态的默认IP与端口设置初始值 (定义)
*/
const std::string UdpServer::defaultip = "0.0.0.0";
const uint16_t UdpServer::defaultport = 8080;
#endif
在该实现中引用了之前的Log
日志插件,具体参考[Gitee - MyLogPlug];
引入了一系列的头文件用于支持后续需要的实现;
定义了一系列的成员变量包括服务端的套接字文件描述符sockfd_
,服务端进程端口号port_
,服务端IP
地址ip_
,以及表明服务端是否运行的标识符isrunning_
;
其中设置了默认的服务端进程端口号与服务端的IP
地址;
构造函数用于初始化服务端的成员变量,析构函数调用close
关闭对应的套接字文件描述符;
声名了两个函数作为服务端的主要功能:
-
Init()
该函数用于实现服务端中套接字的初始化以及将套接字绑定到指定的
IP
地址和端口号; -
Run()
该函数用于实现服务端的运行以及处理客户端所发的数据包;
Init() 服务端的初始化
服务端的初始化主要分为两个步骤,即创建服务端的套接字以及将套接字绑定到指定的IP
地址和端口号;
-
服务端的套接字创建
void Init() { /* 1. 创建 UDP socket */ sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 表示使用IPv4 UDP协议 // SOCK_DGRAM 表示允许一个面向数据报 无连接 不可靠的数据传输 if (sockfd_ < 0) { log_(FATAL, "socket create fail , the errornum : %d\n", sockfd_); // 打印日志信息确认套接字创建是否成功 exit(SOCK_CREATE_FAIL); // 套接字创建失败时退出 } log_(INFO, "socket create sucess , sockfd : %d", sockfd_); /* 2. bind 绑定 */ // ... }
该函数中调用了
socket()
函数创建了一个套接字;sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
其中参数
AF_INET
表示使用IPv4
协议,SOCK_DGRAM
表示该套接字允许一个面向数据报且无连接同时不保证可靠性的数据传输;if (sockfd_ < 0)...
当套接字创建失败时会返回
-1
,当套接字创建失败时接下来的操作都将不可进行否则可能会出现未定义行为,此时需要打印日志信息并退出进程; -
服务端的套接字绑定
void Init() { /* 1. 创建 UDP socket */ //... /* 2. bind 绑定 */ // 2.1 类似于设置阻塞信号集 此处只是设置了一个变量 struct sockaddr_in localsr; bzero(&localsr, sizeof(localsr)); localsr.sin_family = AF_INET; // 表明结构体地址类型 localsr.sin_port = htons(port_); // 表明需要绑定的端口 // localsr.sin_addr.s_addr = inet_addr(ip_.c_str()); // // 表明需要绑定的IP地址 localsr.sin_addr.s_addr = INADDR_ANY; // bind 地址为0时表示可收到来自所有主机的数据 是一种比较推荐的做法 /* 其中端口号和IP地址必须是网络字节序的 (IP与端口必定是客户端和服务端互相发送的) 使用 htons 用于 uint16_t 的转网络字节序 sin_addr是一个结构体 该结构体的s_addr成员才是需要填入的IP 使用 inet_addr 用于 const char* 类型的IP 转 uint16_t(网络字节序) */ // 2.2 进行 bind if (bind(sockfd_, (const struct sockaddr *)&localsr, sizeof(localsr))) { log_(FATAL, "socket bind fail, err string :%s", strerror(errno)); exit(SOCK_BIND_FAIL); } // bind 成功 log_(INFO, "socket bind sucess , sockfd : %d", sockfd_); }
绑定的操作流程与设置阻塞信号集相似,需要创建一个对应地址族的结构体,为该结构体进行初始化而后才能进行绑定;
struct sockaddr_in localsr; bzero(&localsr, sizeof(localsr));
创建了一个
sockaddr_in
类型的对象localsr
并调用bzero
初始化该结构体;同样的该结构体需要表明结构体地址类型与表明需要绑定的端口以及表明需要绑定的
IP
地址;localsr.sin_family = AF_INET; // 表明结构体地址类型 localsr.sin_port = htons(port_); // 表明需要绑定的端口 // localsr.sin_addr.s_addr = inet_addr(ip_.c_str()); // // 表明需要绑定的IP地址 localsr.sin_addr.s_addr = INADDR_ANY; // bind 地址为0时表示可收到来自所有主机的数据 是一种比较推荐的做法
云服务器不允许被直接绑定,
bind
云服务器的公网IP
时将会bind error
;通常情况下服务端绑定的
IP
地址为0
(INADDR_ANY
)是比较推荐的做法,表示接收任何主机发送过来的数据包;当对应的结构体设置完毕后可进行
bind
绑定;if (bind(sockfd_, (const struct sockaddr *)&localsr, sizeof(localsr))) ...
此处在绑定时还判断了一次绑定是否成功,若绑定未成功则打印对应日志消息并退出进程(绑定失败为致命操作);
Run() 服务端的运行
服务端是需要一直运行的,所以在启动服务端后需要有一个对应的标识服务器是否启动的标识;
服务端的运行主要为维持服务端的运行,对数据进行处理;
void Run() {
// 服务器需要一直运行
isrunning_ = true;
char inbuf[BUF_SIZE] = {0};
while (isrunning_) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
bzero(&client, len);
// 使用 recvfrom 接收来自客户端发送的消息
size_t n = recvfrom(sockfd_, inbuf, sizeof(inbuf) - 1, 0,
(struct sockaddr *)&client, &len);
// client 与 len 保存着客户端发送过来的套接字信息
if (n < 0) {
log_(WARNING, "recvfrom fail, err string :%s", strerror(errno));
continue;
}
inbuf[n] = 0; // 当字符串进行打印
// 充当数据处理
std::string info = inbuf; // 组合字符串
std::string echo_string = "server echo# " + info;
sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0,
(const struct sockaddr *)&client, len);
// 将数据发回给客户端
}
}
该程序为一个简单的echo
打印程序,主要逻辑为获取客户端发来的数据并进行简单处理,再将处理后的数据发回客户端进行显示;
定义了一个缓冲区char inbuf[BUF_SIZE] = {0};
;
在接收客户端的数据时同样需要一个相同地址族的结构体,与对应的长度信息,两个信息作为输出型参数用于接收客户端发送的套接字信息;
while (isrunning_) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
bzero(&client, len);
//...
}
调用bzero
对结构体进行初始化;
调用recvfrom
接收来自客户端发送过来的数据包并存储进对应的缓冲区中;
// 使用 recvfrom 接收来自客户端发送的消息
size_t n = recvfrom(sockfd_, inbuf, sizeof(inbuf) - 1, 0,
(struct sockaddr *)&client, &len);
将数据简单进行组合作为数据的简单处理随后将数据发回客户端;
-
服务端运行与数据处理的解耦合
该段程序中对于数据处理和服务端运行(接收数据)耦合度过高,可以使用传递回调的方式进行解耦合;
using func_t = std::function<std::string(const std::string &)>; // 使用function包装器包装一个函数类型 用于接收传入的处理数据的函数 class UdpServer { public: UdpServer(const uint16_t port = defaultport) : sockfd_(0), port_(port), isrunning_(false) {} ~UdpServer() { // ... } void Init() { // ... } void Run(func_t fun) { // 传入一个回调函数 // ... size_t n = recvfrom(sockfd_, inbuf, sizeof(inbuf) - 1, 0, (struct sockaddr *)&client, &len); inbuf[n] = 0; // 充当数据处理 std::string info = inbuf; // 组合字符串 std::string echo_string = fun(inbuf); // 调用回调函数进行数据处理 std::cout << echo_string << std::endl; // 打印处理好的数据... } } private: // 成员变量 };
对应的
main
函数传入一个函数进行回调即可;
服务端启动及测试
#include <iostream>
#include "UdpServer.hpp"
using namespace std;
void Usage(std::string proc) {
// 使用手册
std::cout << "\n\tUsage: " << proc << " port[1024+]\n" << std::endl;
}
string func(const string& str) { // 定义一个函数用于数据的处理
std::string echo_string = "server echo# " + str;
return echo_string;
}
int main(int argc, char* argv[]) {
if (argc != 2) {
// 判断传入参数是否符合使用标准
Usage(argv[0]);
exit(0);
}
std::unique_ptr<UdpServer> svr(new UdpServer(stoi(argv[1]))); // 守卫智能指针
svr->Init();
svr->Run(func); // 传入处理数据函数
return 0;
}
创建了一个函数Usag
用于当用户传入的环境变量参数不符合标准时调用该函数展示对应的使用手册;
定义了一个函数用于数据的处理,该函数将作为可调用对象传入Run
成员函数中;
使用防拷贝智能指针unique
实例化一个服务端对象指针;
随后调用服务端的Init()
与Run()
进行初始化与启动服务端;
运行结果为:
$ ./udpserver 8080
[INFO][2024-08-14 23:44:22] socket create sucess , sockfd : 3
[INFO][2024-08-14 23:44:22] socket bind sucess , sockfd : 3
程序正常运行,可调用netstat -naup
查看对应网络信息;
$ netstat -naup
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
udp 0 0 0.0.0.0:68 0.0.0.0:* -
udp 0 0 127.0.0.1:323 0.0.0.0:* -
udp 0 0 0.0.0.0:8080 0.0.0.0:* 12440/./udpserver
udp6 0 0 ::1:323 :::* -
其中12440/./udpserver
表示运行成功;
启动服务端后可用nc
工具向对应的服务端发送数据测试服务端是否有效;
# 服务端所在会话 - 启动服务端
$ ./udpserver 8000
[INFO][2024-08-15 14:36:40] socket create sucess , sockfd : 3
[INFO][2024-08-15 14:36:40] socket bind sucess , sockfd : 3
# 客户端所在会话(非同一网络的其他主机) - 通过 nc 工具发送消息给服务端和 server
$ echo "Hello, Server!" | nc -u xxx.xxx.xxx.xxx 8000 # xxx... 表示IP地址
# 服务端所在会话
[INFO][2024-08-15 14:51:29] recvfrom sucess
server echo# Hello, Server!
简单的UDP网络程序客户端
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <memory>
#include <string>
void Usage(std::string proc) {
// 使用手册
std::cout << "\n\tUsage: " << proc << " serverip serverport\n" << std::endl;
}
int main(int argc, char* argv[]) {
// 判断环境变量参数是否符合使用标准
if (argc != 3) {
Usage(argv[0]);
exit(0);
}
/*
分解环境变量参数并进行转换为 IP 和端口
*/
std::string serverip = argv[1]; // IP
uint16_t serverport = std::stoi(argv[2]); // 端口
sockaddr_in local; // 创建对应的地址族结构体
bzero(&local, sizeof(local)); // 为结构体清零
local.sin_family = AF_INET; // 设置地址族为 IPv4
local.sin_port = htons(serverport); // 将端口号转换为网络字节序
local.sin_addr.s_addr =
inet_addr(serverip.c_str()); // 将IP地址转换为网络字节序并赋值
socklen_t locallen = sizeof(local); // 获取地址结构体的大小
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 创建UDP套接字并保存文件描述符
if (sockfd < 0) {
// 判断套接字是否创建成功
std::cout << "socket fail" << std::endl;
exit(-1);
}
std::cout << "socket success" << std::endl;
// 通常客户端不需要显式绑定自己的IP和端口
// 一个端口号只能被一个进程bind,客户端端口号只需保证唯一性
std::string message; // 用于保存发送给服务端的数据
char buffer[1024] = {0}; // 用于接收服务端返回的数据
while (true) {
std::cout << "Please Enter@"; // 打印提示信息
getline(std::cin, message); // 读取用户输入的消息
int sdebug = sendto(sockfd, message.c_str(), message.size(), 0,
(struct sockaddr*)&local, locallen);
// 调用 sendto 向服务端发送数据 // sdebug 为debug
if (sdebug < 0) {
std::cout << "sendto fail, err: " << strerror(errno) << std::endl;
}
std::cout << "sendto success" << std::endl; // 发送成功
// -------------------------------
struct sockaddr_in temp; // 用于存储服务端的地址信息
bzero(&temp, sizeof(temp)); // 清空结构体
socklen_t len = sizeof(temp);
ssize_t n =
recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
// 阻塞接收服务端的回应数据
if (n > 0) {
buffer[n] = 0; // 将接收到的数据作为字符串处理
std::cout << buffer << std::endl;
}
}
close(sockfd); // 关闭套接字文件描述符
return 0;
}
客户端并未进行封装;
客户端主要进行两个操作,一个是向服务端发送数据,一个是接收服务端返回的处理后的数据包;
同样的定义了一个函数用于当参数传入不同时展现其使用手册;
std::string serverip = argv[1]; // IP
uint16_t serverport = std::stoi(argv[2]); // 端口
将传入的IP
与端口号进行分离,便于后期sendto
向服务端发送数据时使用;
sockaddr_in local; // 创建对应的地址族结构体
bzero(&local, sizeof(local)); // 为结构体初始化
local.sin_family = AF_INET; // 设置地址族为 IPv4
local.sin_port = htons(serverport); // 将端口号转换为网络字节序
local.sin_addr.s_addr =
inet_addr(serverip.c_str()); // 将IP地址转换为网络字节序并赋值
socklen_t locallen = sizeof(local); // 获取地址结构体的大小
根据地址族创建对应的地址族结构体并为这个地址族结构体进行初始化;
-
客户端不需要显式
bind
端口号通常情况下客户端不需要显式
bind
端口号,原因是防止特别的端口号或者被其他进程绑定了的端口号被占用(一个端口号只能被一个进程bind
);当客户端在进行
sendto
操作时操作系统将自行为进程动态分配端口号;这个端口号一般是由操作系统自由随机选择的;
此处的客户端模拟循环操作,即一个循环的会话,客户端可向服务端发送数据,服务端进行打印;
std::string message; // 用于保存发送给服务端的数据
char buffer[1024] = {0}; // 用于接收服务端返回的数据
while (true) {
std::cout << "Please Enter@"; // 打印提示信息
getline(std::cin, message); // 读取用户输入的消息
...
...
}
创建一个套接字用于接收服务端处理并返回的数据,通过recvfrom
函数接收;
struct sockaddr_in temp; // 用于存储服务端的地址信息
bzero(&temp, sizeof(temp)); // 清空结构体
socklen_t len = sizeof(temp);
ssize_t n =
recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
可以在循环中设置特定操作用于退出客户端;
服务端客户端相互通信测试
分别在不同的主机上运行服务端和客户端;
# 服务端启动
$ ./udpserver 8000
[INFO][2024-08-15 15:26:10] socket create sucess , sockfd : 3
[INFO][2024-08-15 15:26:10] socket bind sucess , sockfd : 3
# 客户端启动并发送信息
$ ./udpclient xxx.xxx.xxx.xxx 8000
Please Enter@hello server!
server echo# hello server!
# 服务端接收信息
[INFO][2024-08-15 15:26:49] recvfrom sucess
server echo# hello server!
网络通信成功;
服务端通过传入命令处理实现远程命令执行
可以通过实现命令解析并处理的功能实现不同网络情况下对对端主机进行命令处理;
在此之前服务端的运行与数据的处理已经进行了解耦合;
现在只需要实现命令的解析及处理并传入服务端成员函数Run
中就可以实现该功能;
std::string HandlerCommand(const std::string& cmd) {
// 打开管道,执行命令
FILE* fp = popen(cmd.c_str(), "r");
if (!fp) {
perror("popen");
return "error";
}
std::string ret;
char buffer[4096];
// 循环读取命令输出
while (true) {
char* res = fgets(buffer, sizeof(buffer), fp);
if (res == nullptr) break; // 到达文件末尾,或出错
ret += std::string(buffer); // 将命令输出追加到返回字符串中
}
// 关闭管道,并获取命令执行的返回值
int status = pclose(fp);
if (status == -1) {
perror("pclose");
return "error";
}
// 返回命令执行结果
return ret;
}
通过使用popen
打开管道执行命令;
使用fgets
循环读取fp
指针中的命令执行内容,将命令输出追加到返回字符串ret
中;
最后使用结束后调用pclose
关闭管道,随后返回命令执行结果;
在使用该数据处理函数时只需要将该可调用对象传入Run
成员函数即可;
int main(int argc, char* argv[]) {
if (argc != 2) {
Usage(argv[0]);
exit(0);
}
std::unique_ptr<UdpServer> svr(new UdpServer(stoi(argv[1])));
svr->Init();
svr->Run(HandlerCommand); // 传入 HandlerCommand
return 0;
}
测试如下:
# 服务端启动
$ ./udpserver 8000
[INFO][2024-08-15 15:52:45] socket create sucess , sockfd : 3
[INFO][2024-08-15 15:52:45] socket bind sucess , sockfd : 3
# 客户端启动并使用命令
Please Enter@ls
log.hpp
Main.cc
Makefile
noMyUDP
udpclient
UdpClient.cc
udpserver
UdpServer.hpp
Please Enter@
# 服务端接收
[INFO][2024-08-15 15:55:38] recvfrom sucess
log.hpp
Main.cc
Makefile
noMyUDP
udpclient
UdpClient.cc
udpserver
UdpServer.hpp
参考代码
[Gitee - 半介莽夫 / Dio夹心小面包]