主要内容:简述网络传输流程(TCP/IP五层模式概念认知,Mac地址、端口号、网络字节序等),演示socke套接字编程(UDP模式)。
1)、网络发展说明
独立模式: 计算机之间相互独立;
网络互联: 多台计算机连接在一起, 完成数据共享;
局域网LAN: 计算机数量更多了, 通过交换机和路由器连接在一起。
广域网WAN: 将远隔千里的计算机都连在一起;
PS:所谓 “局域网” 和 “广域网” 只是一个相对的概念.。
1)、协议分层
计算机之间的传输媒介是光信号和电信号,通过 “频率” 和 “强弱” 来表示 0 和 1 这样的信息。要想传递各种不同的信息,就需要约定好双方的数据格式。
实际的网络通信会需要分更多的层次,分层最大的好处在于 “封装”。
2)、OSI七层模型
OSI(Open System Interconnection,开放系统互连) 七层网络模型称为开放式系统互联参考模型,是一个逻辑上的定义和规范。把网络从逻辑上分为了7层,每一层都有相关、相对应的物理设备,比如路由器,交换机。
OSI 七层模型是一种框架性的设计方法,其最主要的功能使就是帮助不同类型的主机实现数据传输。它的最大优点是将服务、接口和协议这三个概念明确地区分开来,概念清楚,理论也比较完整,通过七个层次化的结构模型,使不同的系统不同的网络之间实现可靠的通讯。但是, 它既复杂又不实用,故通常按照TCP/IP五层模型来处理。
名称 | 功能 | |
---|---|---|
7 | 应用层 | 针对特定应用的协议。 |
6 | 表示层 | 设备固有数据格式和网络标准数据格式的转换。 |
5 | 会话层 | 通信管理。负责建立和断开通信连接(数据流动的逻辑通路)。管理传输层以下的分层。 |
4 | 传输层 | 管理两个节点之间的数据传输。负责可靠传输(确保数据被可靠地传送到目标地址)。 |
3 | 网络层 | 地址管理与路由选择。 |
2 | 数据链路层 | 互连设备之间传送和识别数据帧。 |
1 | 物理层 | 以“0”、 “1”代表电压的高低、灯光的闪灭。界定连接器和网线的规格。 |
3)、TCP/IP五层(或四层)模型
TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇.。
TCP/IP通讯协议采用了5层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求。
名称 | 功能 | 具体说明 |
---|---|---|
应用层 | 负责应用程序间沟通 | 如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。 |
传输层 | 负责两台主机之间的数据传输。 | 如传输控制协议 (TCP), 能够确保数据可靠的从源主机发送到目标主机。 |
网络层 | 负责地址管理和路由选择。 | 例如在IP协议中, 通过IP地址来标识一台主机, 并通过路由表的方式规划出两台主机之间的数据传输的线路(路由)。路由器(Router)工作在网路层。 |
数据链路层 | 负责设备之间的数据帧的传送和识别。 | 例如网卡设备的驱动、帧同步(就是说从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作。有以太网、令牌环网, 无线LAN等标准。交换机(Switch)工作在数据链路层。 |
物理层 | 负责光/电信号的传递方式。 | 比如现在以太网通用的网线(双绞线)、早期以太网采用的的同轴电缆(现在主要用于有线电视)、光纤, 现在的wifi无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等。集线器(Hub)工作在物理层。 |
2)、TCP/IP通讯过程
同一个网段内的两台主机可以进行文件传输(局域网中两台主机是可以直接通信的)。虽然在通讯双方看来是彼此之间的通信(应用层->应用层),实际其经过了一个自顶向下,又自底向上的过程(应用层到底层,底层到应用层)。
3)、数据包封装和分用
不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报 (datagram),在链路层叫做帧(frame)。
应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装(Encapsulation)。首部信息中包含了一些类似于首部有多长,载荷(payload)有多长,上层协议是什么等信息。数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,根据首部中的 “上层协议字段” 将数据交给对应的上层协议处理。
1)、为什么局域网中两台主机能相互通信?如何通信?
MAC地址: 用来识别数据链路层中相连的节点。 长度为48位,即6个字节。一般用16进制数字加上冒号的形式来表示(例如: 08:00:27:03:fb:19
)。MAC地址通常是唯一的,在网卡出厂时就已确定,不能修改。
PS:①虚拟机中的mac地址不是真实的mac地址,可能会冲突。②也有些网卡支持用户配置mac地址。
ifconfig
指令:可以查看Linux系统中的相关配置属性(IP地址、Mac地址)
2)、假如两台主机不在同一局域网中,如何通信?
跨网段的主机的文件传输方式:数据从一台计算机到另一台计算机传输过程中要经过一个或多个路由器。
IP地址: : IP协议有两个版本,IPv4和IPv6。IP地址是在IP协议中,用来标识网络中不同主机的地址。 对于IPv4来说,P地址是一个4字节,32位的整数。
我们通常也使用 “点分十进制” 的字符串表示IP地址,例如:192.168.0.1
,用点分割的每一个数字表示一个字节,范围是 0 - 255
。
PS:在IP数据包头部中,有两个IP地址,分别叫做源IP地址,和目的IP地址。 数据要从源IP到目的IP,则需要从源Mac地址,途经各个下一站Mac地址。
1)、问题引入
问题:对当前主机而已,把自身的数据送到对方的主机,是最终目的吗?
回答:并不是。真正的网络通信过程,本质其实是进程间通信。(例如:客户端进程、服务器进程)。
将数据在主机间转发仅仅是用于完成通信的手段,对方主机接收到数据之后,需要将数据交付给指定的进程! 而OS中可有多个进程同时运行,如何确定将数据交给哪一个进程?因此,引入端口号。
2)、概念与相关说明
端口号(port):传输层协议的内容,是一个2字节16比特位的整数。用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理。
端口号具有唯一性,一个端口号只能被一个进程占用。IP地址 + 端口号
,能够标识网络上的某一台主机的某一个进程。
说明一:“端口号” 和 “进程ID”
①二者都具唯一性,但一个是网络模块,一个是进程管理模块。不是不能互相只用一个,但这样一来会将两个不同模块之间关联起来,不如各自搞各自的一套执行方案,实现数据解耦。
②并非所有进程都需要网络通信(端口号),一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定。
说明二:源端口号和目的端口号
①传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号。就是在描述 “数据是谁发的, 要发给谁”(谁发数据,谁就是源端口号;谁接收数据,谁就是目的端口号)。
1)、各自特点
TCP协议:Transmission Control Protocol 传输控制协议。
传输层协议
有连接
可靠传输
面向字节流
(关于可靠、不可靠:不能肤浅的字面理解。如数据丢包之类,有些场景丢包影响并不大,而可靠的背后代表着为了数据安全该协议会做大量处理工作,增加工作量和维护成本。)
UDP协议:User Datagram Protocol 用户数据报协议。
传输层协议
无连接
不可靠传输
面向数据报
1)、基本说明
内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据。如果当前发送主机是小端,就需要先将数据转成大端,否则就忽略直接发送即可。
2)、用于做网络字节序和主机字节序转换的函数
为使网络程序具有可移植性、使同样的C代码在大端和小端计算机上编译后都能正常运行。可以调用以下库函数做网络字节序和主机字节序的转换。
NAME
htonl, htons, ntohl, ntohs - convert values between host and network byte order
SYNOPSIS
#include
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。hton:主机传网络;ntoh:网络传主机。
l
表示32位长整数(unsigned integer),s
表示16位短整数(unsigned short integer)。
例如:htonl
表示将32位的长整数从主机字节序转换为网络字节序。实际场景举例:将IP地址转换后准备发送。
DESCRIPTION
The htonl() function converts the unsigned integer hostlong from host byte order to network byte order.
The htons() function converts the unsigned short integer hostshort from host byte order to network byte order.
The ntohl() function converts the unsigned integer netlong from network byte order to host byte order.
The ntohs() function converts the unsigned short integer netshort from network byte order to host byte order.
On the i386 the host byte order is Least Significant Byte first, whereas the network byte order, as used on the Internet, is Most Sig‐
nificant Byte first.
以下内容为小节3、4涉及,这里只是将其总结性拎出方便查阅。
1)、整体说明
说明: socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6等。理论上有三套套接字,分别对应不同场景, 各种网络协议的地址格式并不相同。但OS在设计时统一使用sockaddr
接口,再根据传参,sockaddr
结构体的首地址(地址类型字段)可以确定究竟是哪一类型套接字,从而指向对应套接字结构体中的内容。
1、IPv4
和IPv6
的地址格式定义在netinet/in.h
中。IPv4
地址用sockaddr_in
结构体表示,包括16位地址类型、16位端口号和32位IP地址.
2、IPv4
、IPv6
地址类型分别定义为常数AF_INET
、AF_INET6
(宏)。 这样设置的好处在于:只要取得某种sockaddr
结构体的首地址,就可以根据地址类型字段确定结构体中的内容(即:不需要知道具体的sockaddr
结构体内部格式).
3、socket API
可以都用struct sockaddr *
类型表示,在使用的时候需要强制转化成sockaddr_in
。这样的好处在于程序的通用性,可以接收IPv4
、IPv6
以及UNIX Domain Socket
各种类型的sockaddr
结构体指针做为参数。
2)、一些常见的地址格式(内部实现简览)
sockaddr
结构:
/* Structure describing a generic socket address. */
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
sockaddr_in
结构: 该结构里主
要有三部分信息: 地址类型, 端口号, IP地址.
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
in_addr
结构:用来表示一个IPv4的IP地址, 其实就是一个32位的整数。
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
sockaddr_in6
结构:
#if !__USE_KERNEL_IPV6_DEFS
/* Ditto, for IPv6. */
struct sockaddr_in6
{
__SOCKADDR_COMMON (sin6_);
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};
#endif /* !__USE_KERNEL_IPV6_DEFS */
#pragma once
#include
#include
#include
#include
#include
// 日志级别
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
const char *gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
// 完整的日志功能,至少有 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)//const char *format, ... 可变参数
{
#ifndef DEBUG_SHOW
if(level== DEBUG) return;
0#endif
//标准部分:固定输出的内容
char stdBuffer[1024];
time_t timestamp = time(nullptr);
snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);
//自定义部分:允许用户根据自己的需求设置
char logBuffer[1024];
va_list args; //定义一个va_list对象
va_start(args, format);
vsnprintf(logBuffer, sizeof logBuffer, format, args);
va_end(args); //相当于 args == nullptr
printf("%s%s\n", stdBuffer, logBuffer);
}
以下为服务器基本框架搭建:使用时,只需要在udp_server.cc端调用InitServer
、Start
函数,那么服务端的程序运行时,就会得到以供客户端传送消息的服务器。
class UdpServer
{
public:
// 构造:将对应的端口号、IP传入
UdpServer(uint16_t port, std::string ip = "")//对ip默认值说明:方便后续bind操作,1、可从任意IP获取数据(默认情况),2、也可指定需要的IP(自己传入参数的情况)
: port_(port), ip_(ip), sock_(-1)
{
}
// 析构:关闭套接字
~UdpServer()
{
if (sock_ >= 0)
close(sock_);
}
//初始化服务器
bool InitServer()
{
}
//启动服务器
void Start()
{
}
private:
uint16_t port_; // 端口号:16位的整数
std::string ip_; // IP地址:点分十进制字符串风格
int sock_; // 通讯时的套接字:需要供多处使用
};
1)、相关函数介绍
man socket
可查看该函数,注意其包含的头文件。
NAME
socket - create an endpoint for communication
SYNOPSIS
#include /* See NOTES */
#include
int socket(int domain, int type, int protocol);
DESCRIPTION
socket() creates an endpoint for communication and returns a descriptor.
protocol
:协议。一般只要前两个参数确定好,那么对应的协议也就确定了。(忽略可填0
)
domain
:套接字的域。以下列举出的最常使用的三种类型,IPv4、IPv6、本地通讯。(其它可查阅文档)
The domain argument specifies a communication domain; this selects the protocol family which
will be used for communication. These families are defined in <sys/socket.h>. The currently
understood formats include:
Name Purpose Man page
AF_UNIX, AF_LOCAL Local communication unix(7)
AF_INET IPv4 Internet protocols ip(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
type
:通讯种类。这里也只列举了常用的两个类型,面向数据报(UDP)模式,以及面向流式(TCP)。其它可查阅文档。
The socket has the indicated type, which specifies the communication semantics. Currently
defined types are:
//面向流式
SOCK_STREAM Provides sequenced, reliable, two-way, connection-based byte streams. An out-
of-band data transmission mechanism may be supported.
//面向数据报
SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a fixed maximum
length).
返回值
:若成功,则返回一个file descriptor
,即文件描述符(指向对应的套接字)。若失败则返回-1,并设置错误码。
RETURN VALUE
On success, a file descriptor for the new socket is returned. On error, -1 is returned, and
errno is set appropriately.
2)、创建套接字
写法如下:
//1、创建套接字
sock_ = socket(AF_INET, SOCK_DGRAM, 0);
if(sock_ < 0)
{
logMessage(ERROR,"%d:%s ", errno, strerror(errno));
exit(2);
}
logMessage(DEBUG, "创建套接字成功,sock: %d ",sock_);
1)、相关函数介绍
bind
man bind
:当使用socket()
创建套接字时,它存在于命名空间(地址族)中,但没有地址分配给它。bind()
将addr指定的地址分配给文件描述符引用的套接字。
NAME
bind - bind a name to a socket
SYNOPSIS
#include /* See NOTES */
#include
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
DESCRIPTION
When a socket is created with socket(2), it exists in a name space (address family) but has no address
assigned to it. bind() assigns the address specified by addr to the socket referred to by the file descriptor
sockfd. addrlen specifies the size, in bytes, of the address structure pointed to by addr. Traditionally,
this operation is called “assigning a name to a socket”.
It is normally necessary to assign a local address using bind() before a SOCK_STREAM socket may receive con‐
nections (see accept(2)).
返回值: 成功返回0,失败返回-1。
RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
参数说明:
sockfd
:来源于socket函数创建的返回值。
addrlen
:即设置的sockaddr结构体的大小。
const struct sockaddr *addr
:sockaddr结构体,根据之前2.1小节内容,这里使用的是统一的接口,但实际需要根据我们的需求来设置。(如:IPv4为sockaddr_in
,所以使用时存在类型转换问题。)
我们定义出sockaddr_in结构体,是以供bind函数使用。在server.hpp文件中,该结构体填充的是服务器的端口号和IP地址。(PS:实际网络通信过程中,有时服务器需要对客户端发送的请求做出响应,故也需要将其IP地址和端口号告知客户端。而我们知道网络也存在字节序问题,因此,这里sockadr_in填入的数据,需要涉及一定转换。)
bzore
用于清零:这里主要用于sockaddi_in
结构体中,末尾8字节填充位。实际也可以调用其成员对象sin_zero。或者使用void *memset(void *s, int c, size_t n);
NAME
bzero - write zero-valued bytes
SYNOPSIS
#include
void bzero(void *s, size_t n);
DESCRIPTION
The bzero() function sets the first n bytes of the area starting at s to zero (bytes containing '\0').
RETURN VALUE
None.
htons、inet_addr
htons
:网络字节序转换的函数。
说明:这里主要用于port端口号(大小通常为16位)。服务器的IP和端口未来也是要发送给客户端主机,因此要先将数据发送到网络中,而发送到网络时涉及网络字节序。
SYNOPSIS
#include
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
DESCRIPTION
The htonl() function converts the unsigned integer hostlong from host byte order to network byte
order.
The htons() function converts the unsigned short integer hostshort from host byte order to network
byte order.
inet_addr
:一套接口,可以一次做完两件事情。①、将点分十进制字符串风格的IP地址 -> 4字节;②、4字节主机序列 -> 网络序列。 int_addr_t
为uint32_t
。
说明: 通常,为了阅读性,IP地址以点分十进制字符串风格表示,例如"192.168.110.132"
,每一个区域取值范围是[0,255]
,刚好对应1字节。而三个.
将IP地址划分为4个区域,理论上,4字节就可以存储IP地址。(若直接传递字符串,一共有15个字符,即15字节,增大数据大小。)
SYNOPSIS
#include
#include
#include
in_addr_t inet_addr(const char *cp);
DESCRIPTION
The inet_addr() function converts the Internet host address cp from IPv4 numbers-and-dots notation
into binary data in network byte order. If the input is invalid, INADDR_NONE (usually -1) is
returned. Use of this function is problematic because -1 is a valid address (255.255.255.255).
Avoid its use in favor of inet_aton(), inet_pton(3), or getaddrinfo(3) which provide a cleaner way
to indicate error return.
2)、 bind: 将用户设置的ip和port在内核中和当前的进程强关联
写法如下:
// 2、bind绑定:
// 2.1、绑定前的准备工作:
struct sockaddr_in localaddr;
bzero(&localaddr, sizeof localaddr); // 将结构体清零
localaddr.sin_family = AF_INET; // 告知通讯方式,通常与domain同
localaddr.sin_port = htons(port_); // 端口号:注意转为网络字节序
localaddr.sin_addr.s_addr = ip_.empty() ? INADDR_ANY : inet_addr(ip_.c_str()); // IP:点分十进制->四字节+网络字节序
// 2.2、绑定:将用户设置的ip和port在内核中和当前的进程强关联
if (bind(sock_, (struct sockaddr *)&localaddr, sizeof localaddr) < 0)
{
logMessage(ERROR,"%d:%s",errno,strerror(errno));
exit(2);
}
logMessage(DEBUG,"绑定成功,初始化服务器完成!");
关于INADDR_ANY
:让服务器在工作过程中,可以从任意IP中获取数据。(例如同一台机器有多个网卡,那么,当发送数据给当前主机时,只要端口号确定,任意IP传来的数据都接收。)
/* Address to accept any incoming messages. */
#define INADDR_ANY ((in_addr_t) 0x00000000)
1)、相关函数介绍
NAME
recv, recvfrom, recvmsg - receive a message from a socket
SYNOPSIS
#include
#include
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
DESCRIPTION
The recvfrom() and recvmsg() calls are used to receive messages from a socket, and may be used to
receive data on a socket whether or not it is connection-oriented.
If src_addr is not NULL, and the underlying protocol provides the source address, this source
address is filled in. When src_addr is NULL, nothing is filled in; in this case, addrlen is not
used, and should also be NULL. The argument addrlen is a value-result argument, which the caller
should initialize before the call to the size of the buffer associated with src_addr, and modified
on return to indicate the actual size of the source address. The returned address is truncated if
the buffer provided is too small; in this case, addrlen will return a value greater than was sup‐
plied to the call.
返回值:
RETURN VALUE
These calls return the number of bytes received, or -1 if an error occurred. In the event of an
error, errno is set to indicate the error. The return value will be 0 when the peer has performed
an orderly shutdown.
参数介绍:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
sockfd
,即套接字(socket)
buf、len
:读取数据,就要有对应缓冲区来接收(缓冲区及缓冲区大小)。
flags
:读取数据的方式,默认0为阻塞读取。
src_addr
:纯输出型参数。 除了接收数据,服务端也想知道给它发消息的客户端谁(即通讯是双向的,有来有回)。故这里的参数是为了获取客户端IP和端口号。src_ ip源IP、src_ port源端口号。
addrlen
:输入输出型参数。 输入时,一般填充src_addr的大小。输出时,会被设置为实际读到的src_addr大小。
2)、recvfrom:服务器不断接收来子客户端的数据
首次演示,这里接收、发送的都是字符串数据,处理时也只是在服务端打印于显示器上。PS:这里服务端具体做什么业务处理,要根据需求而定。
//1、作为一款网络服务器,永远不退出地在接收客服端通过网络传来的请求
while(true)
{
//1.1、准备工作:
//a、用于后续从网络中读取客户端的IP、端口号
struct sockaddr_in clientaddr;
bzero(&clientaddr,sizeof clientaddr);
socklen_t len = sizeof clientaddr;//输入输出型参数:输入时传递的是clientaddr当前定义的大小,使用是会输出实际大小
//b、用于存储数据
char server_buffer[SIZE];
//1.2、读取数据:
ssize_t s = recvfrom(sock_,server_buffer,strlen(server_buffer)-1,0,(struct sockaddr*)&clientaddr,&len);//注意对clientaddr类型转换
if(s > 0 )//读取成功
{
server_buffer[s]='\0';
//a、获取客户端端口号、IP
uint16_t client_port = ntohs(clientaddr.sin_port);
std::string client_ip=inet_ntoa(clientaddr.sin_addr);
printf("[%s:%d]# %s\n",client_ip.c_str(),client_port,server_buffer);
//b、处理客服端发来的数据请求(自定义TODO)
}
}
补充说明: 上述打印时,我们为了观察,一并获取了客户端的端口号和IP地址,这些数据来源于网络,要在当前进程(服务端)中使用,同样存在网络字节序和主机字节序转换的问题。
ntohs
:网络字节序转换的函数。端口号从网络中读取,需要将网络字节序转换为服务器字节序。
SYNOPSIS
#include
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
DESCRIPTION
The ntohl() function converts the unsigned integer netlong from network byte order to host byte
order.
The ntohs() function converts the unsigned short integer netshort from network byte order to host
byte order.
inet_ntoa
:同理,对IP地址使用。①网络字节序到服务器字节序;②4字节到点分十进制风格;
SYNOPSIS
#include
#include
#include
char *inet_ntoa(struct in_addr in);
DESCRIPTION
The inet_ntoa() function converts the Internet host address in, given in network byte order, to a
string in IPv4 dotted-decimal notation. The string is returned in a statically allocated buffer,
which subsequent calls will overwrite.
1)、相关函数介绍
NAME
send, sendto, sendmsg - send a message on a socket
SYNOPSIS
#include
#include
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
DESCRIPTION
The system calls send(), sendto(), and sendmsg() are used to transmit a message to another socket.
If sendto() is used on a connection-mode (SOCK_STREAM, SOCK_SEQPACKET) socket, the arguments
dest_addr and addrlen are ignored (and the error EISCONN may be returned when they are not NULL and
0), and the error ENOTCONN is returned when the socket was not actually connected. Otherwise, the
address of the target is given by dest_addr with addrlen specifying its size. For sendmsg(), the
address of the target is given by msg.msg_name, with msg.msg_namelen specifying its size.
For send() and sendto(), the message is found in buf and has length len.
返回值:
RETURN VALUE
On success, these calls return the number of characters sent. On error, -1 is returned, and errno is
set appropriately.
参数说明:
sockfd
:用于通信的套接字
buf、len
:需要发送的数据及其大小
flags
:发送的方式,默认可设置为0
dest_addr
:用于存储发送对象的IP和端口号
addrlen
:对应dest_addr的大小。
2)、响应:将处理好的结果返回
由于这里演示时,服务端做的业务处理只是接收数据并将其显示,对应的作为回复,我们将其接收到的信息原封不动传回给客户端即可,也就是echo版服务器。关于这里的dest_addr
,我们不用在根据client_port
、client_ip
进行序列转换,因为clientaddr
中本身就按照网络的需求设置好了。
// 2、响应:将处理好的结果返回。
sendto(sock_, server_buffer, sizeof server_buffer, 0, (struct sockaddr *)&clientaddr, len);
这里还会随着后续通讯的业务处理需求不断调整改变。
#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "log.hpp"
#define SIZE 1024 // 服务端缓冲区大小
class UdpServer
{
public:
// 构造:将对应的端口号、IP传入
UdpServer(uint16_t port, std::string ip = "") // 对ip默认值说明:方便后续bind操作,1、可从任意IP获取数据(默认情况),2、也可指定需要的IP(自己传入参数的情况)
: port_(port), ip_(ip), sock_(-1)
{
}
// 析构:关闭套接字
~UdpServer()
{
if (sock_ >= 0)
close(sock_);
}
// 初始化服务器
bool InitServer()
{
// 1、创建套接字:此处AF_INET也可以是FP_INET
sock_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_ < 0)
{
logMessage(ERROR, "%d:%s ", errno, strerror(errno));
exit(2);
}
logMessage(DEBUG, "创建套接字成功, sock: %d ", sock_);
// 2、bind绑定:
// 2.1、绑定前的准备工作:
struct sockaddr_in localaddr;
bzero(&localaddr, sizeof localaddr); // 将结构体清零
localaddr.sin_family = AF_INET; // 告知通讯方式,通常与domain同
localaddr.sin_port = htons(port_); // 端口号:注意转为网络字节序
localaddr.sin_addr.s_addr = ip_.empty() ? INADDR_ANY : inet_addr(ip_.c_str()); // IP:点分十进制->四字节+网络字节序
// 2.2、绑定:
if (bind(sock_, (struct sockaddr *)&localaddr, sizeof localaddr) < 0) // 注意对localaddr类型转换
{
logMessage(ERROR, "%d:%s", errno, strerror(errno));
exit(2);
}
logMessage(DEBUG, "绑定成功,初始化服务器完成!");
return true;
}
// 启动服务器
void Start()
{
// 1、作为一款网络服务器,永远不退出地在接收客服端通过网络传来的请求
while (true)
{
// 1.1、准备工作:
// a、用于后续从网络中读取客户端的IP、端口号
struct sockaddr_in clientaddr;
bzero(&clientaddr, sizeof clientaddr);
socklen_t len = sizeof clientaddr; // 输入输出型参数:输入时传递的是clientaddr当前定义的大小,使用是会输出实际大小
// b、用于存储数据
char server_buffer[SIZE];
// 1.2、读取数据:
ssize_t s = recvfrom(sock_, server_buffer, sizeof(server_buffer) - 1, 0, (struct sockaddr *)&clientaddr, &len); // 注意对clientaddr类型转换
if (s > 0) // 读取成功
{
server_buffer[s] = 0;
// a、获取客户端端口号、IP:因为是从网络中获取,这里本地显示时需要转换字节序和风格
uint16_t client_port = ntohs(clientaddr.sin_port);
std::string client_ip = inet_ntoa(clientaddr.sin_addr);
printf("[%s:%d]# %s\n", client_ip.c_str(), client_port, server_buffer);
// b、处理客服端发来的数据请求(自定义TODO)
}
// 2、响应:将处理好的结果返回。
sendto(sock_, server_buffer, strlen(server_buffer), 0, (struct sockaddr *)&clientaddr, len);
}
}
private:
uint16_t port_; // 端口号:16位的整数
std::string ip_; // IP地址:点分十进制字符串风格
int sock_; // 通讯时的套接字:需要供多处使用
};
#endif
netstat -anup
:可查看当前存在的所有UDP。
127.0.0.1
:本地环回,client和server发送数据只在本地协议栈中进行数据流动,不会把数据发送到网络中。(通常用于本地网络测试,若使用127.0.0.1可正常通讯,而连接其它网络无法通信,大概率是网络问题)。
PS:①云服务器无法bind公网IP,也不建议。②对server服务器来讲,也不推荐bind确定的IP(上述INADDR_ANY
有举例)。
#include "udp_server.hpp"
#include
//使用手册:当命令行输入错误时,可提示正确启动信息
void Usage(std::string proc)
{
std::cout << "\nUsage:" << proc << " port\n" << std::endl;
}
// 服务端:启动服务端所需指令 upd_server.cc port
int main(int argc, char **argv)
{
// 1、检测输入指令是否正确:关系到服务器启动
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
// 2、获取端口号
uint16_t server_port=atoi(argv[1]);
// 3、使用智能指针来管理服务器
std::unique_ptr<UdpServer> server(new UdpServer(server_port));
server->InitServer();//初始化服务器
server->Start();//启动服务器
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#include
#include "log.hpp"
#define SIZE 1024
// 使用手册:当命令行输入不正确时,可提示正确使用信息
void Usage(std::string proc)
{
std::cout << "\nUsage:" << proc << " server_ip server_port\n"
<< std::endl;
}
// 启动方式:udp_client server_ip server_port要知道服务端的端口号和IP地址
int main(int argc, char **argv)
{
// 1、检测命令行参数是否输入正确
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
// 2、创建套接字
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) // 创建失败
{
std::cerr << "socket error" << std::endl;
exit(2);
}
logMessage(DEBUG, "socket succes. sock: %d", sock);
// 3、进行网络通信
// 3.1、通信前的准备:获取服务端的IP、端口号
struct sockaddr_in serveraddr;
bzero(&serveraddr, sizeof serveraddr);
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2])); // 要向服务器发送数据,要先经过网络,故这里存在字节序的转换(使用与server同)
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
// 3.2、循环式向服务端发出请求并接收响应结果
// PS:首次测试,演示字符型数据传递,echo服务器
while (true)
{
// a、向服务器发送请求
std::string message; // 将交付的信息数据
std::getline(std::cin, message);
if (message == "quit") // string类中有operator==,建议访问官网查阅
break;
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&serveraddr, sizeof serveraddr);
// b、接收来自服务器的响应
char client_buffer[SIZE]; // 用于存储数据
struct sockaddr_in temp; // 临时变量:用于recvfrom中参数(为保证该函数成功使用)
memset(&temp, 0, sizeof temp);
socklen_t len = sizeof temp;
ssize_t s = recvfrom(sock, client_buffer, sizeof client_buffer, 0, (struct sockaddr *)&temp, &len);
if (s > 0) // 接收到实际数据
{
client_buffer[s] = 0;
// echo版服务器,相关后续处理:打印到显示器表示服务端有确切地将数据通过网络sendto到客户端
std::cout << "server echo : " << client_buffer << std::endl;
}
}
return 0;
}
需求说明: 客户端发送指令数据,服务端接收,执行指令,并将其结果返回给客户端。
1)、popen
如何实现: 从命令行参数接收指令并执行,该操作我们在进程控制中学习过。服务端可创建管道,创建子进程,让子进程从管道中读取指令,进行替换执行相关指令。这里我们介绍一个函数,将上述操作一步到位:popen
。
NAME
popen, pclose - pipe stream to or from a process
SYNOPSIS
#include
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
函数说明: 1、创建管道pipe(),创建子进程fork(),exec进程替换执行相关指令command。2、FILE返回值:将执行结果通过FILE*指针读取。
DESCRIPTION
The popen() function opens a process by creating a pipe, forking, and invoking the shell. Since a pipe is by
definition unidirectional, the type argument may specify only reading or writing, not both; the resulting
stream is correspondingly read-only or write-only.
The command argument is a pointer to a null-terminated string containing a shell command line. This command
is passed to /bin/sh using the -c flag; interpretation, if any, is performed by the shell. The type argument
is a pointer to a null-terminated string which must contain either the letter 'r' for reading or the letter
'w' for writing. Since glibc 2.9, this argument can additionally include the letter 'e', which causes the
close-on-exec flag (FD_CLOEXEC) to be set on the underlying file descriptor; see the description of the
O_CLOEXEC flag in open(2) for reasons why this may be useful.
The return value from popen() is a normal standard I/O stream in all respects save that it must be closed
with pclose() rather than fclose(3). Writing to such a stream writes to the standard input of the command;
the command's standard output is the same as that of the process that called popen(), unless this is altered
by the command itself. Conversely, reading from a "popened" stream reads the command's standard output, and
the command's standard input is the same as that of the process that called popen().
Note that output popen() streams are fully buffered by default.
The pclose() function waits for the associated process to terminate and returns the exit status of the com‐
mand as returned by wait4(2).
RETURN VALUE
The popen() function returns NULL if the fork(2) or pipe(2) calls fail, or if it cannot allocate memory.
The pclose() function returns -1 if wait4(2) returns an error, or some other error is detected. In the event
of an error, these functions set errnro to indicate the cause of the error.
2)、strcasestr
说明: 为了防止客户端发送来rm
、rmdir
等系列指令,需要进行一层过滤处理。可以使用此函数,其和strstr类似,只是在找字串时,忽略大小写。
NAME
strstr, strcasestr - locate a substring
SYNOPSIS
#include
char *strstr(const char *haystack, const char *needle);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include
char *strcasestr(const char *haystack, const char *needle);
DESCRIPTION
The strstr() function finds the first occurrence of the substring needle in the string haystack. The termi‐
nating null bytes ('\0') are not compared.
The strcasestr() function is like strstr(), but ignores the case of both arguments.
RETURN VALUE
These functions return a pointer to the beginning of the substring, or NULL if the substring is not found.
实则此部分只需要变动udp_server.hpp中,业务处理部分的逻辑。
// 2、测试二:演示指令执行
// 启动服务器
void Start()
{
// 1、作为一款网络服务器,永远不退出地在接收客服端通过网络传来的请求
while (true)
{
// 1.1、准备工作:
// a、用于后续从网络中读取客户端的IP、端口号
struct sockaddr_in clientaddr;
bzero(&clientaddr, sizeof clientaddr);
socklen_t len = sizeof clientaddr; // 输入输出型参数:输入时传递的是clientaddr当前定义的大小,使用是会输出实际大小
// b、用于存储数据
char server_buffer[SIZE];
// 1.2、读取数据:
ssize_t s = recvfrom(sock_, server_buffer, sizeof(server_buffer) - 1, 0, (struct sockaddr *)&clientaddr, &len); // 注意对clientaddr类型转换
std::string respond;//用于业务处理后,将结果sendto发送给客户端
if (s > 0) // 读取成功
{
server_buffer[s] = 0;
// a、获取客户端端口号、IP:因为是从网络中获取,这里本地显示时需要转换字节序和风格
uint16_t client_port = ntohs(clientaddr.sin_port);
std::string client_ip = inet_ntoa(clientaddr.sin_addr);
// b、处理客服端发来的数据请求(指令执行):读取指令、子进程执行、结果返回
//
// b-1:检测读取到的指令是否为rm系列
if (strcasestr(server_buffer, "rm") != nullptr || strcasestr(server_buffer, "rmdir") != nullptr)
{
std:: string respond = "Wrong:该系列指令无效处理!\n";
std::cout<< respond << "---> 客户端输入指令为: " << server_buffer << std::endl;
sendto(sock_,respond.c_str(), respond.size(), 0, (struct sockaddr *)&clientaddr, len);
continue;
}
// b-2:执行其它指令
char result[SIZE];//从fd(popen返回值)中读取执行后的结果
FILE* fd = popen(server_buffer,"r");//type选项只可以是读或写,不可以同时进行(详细情查看函数)
if (fd == nullptr)// 读取失败
{
logMessage(ERROR, "popen: %d-%s", errno, strerror(errno));
continue;
}
while (fgets(result, sizeof result, fd) != nullptr)
{
respond += result;
}
pclose(fd);
//
}
// 2、响应:将处理好的结果返回。
sendto(sock_, respond.c_str(), respond.size(), 0, (struct sockaddr *)&clientaddr, len);
}
}
改动说明:同理,只需要改变void Start()里业务处理部分的内容。注意需要将结果一一传回。
#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "log.hpp"
#define SIZE 1024 // 服务端缓冲区大小
//演示三:简易版聊天系统(多线程模式)
class UdpServer
{
public:
// 构造:将对应的端口号、IP传入
UdpServer(uint16_t port, std::string ip = "") // 对ip默认值说明:方便后续bind操作,1、可从任意IP获取数据(默认情况),2、也可指定需要的IP(自己传入参数的情况)
: port_(port), ip_(ip), sock_(-1)
{
}
// 析构:关闭套接字
~UdpServer()
{
if (sock_ >= 0)
close(sock_);
}
// 初始化服务器
bool InitServer()
{
// 1、创建套接字:此处AF_INET也可以是FP_INET
sock_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_ < 0)
{
logMessage(ERROR, "%d:%s ", errno, strerror(errno));
exit(2);
}
logMessage(DEBUG, "创建套接字成功, sock: %d ", sock_);
// 2、bind绑定:
// 2.1、绑定前的准备工作:
struct sockaddr_in localaddr;
bzero(&localaddr, sizeof localaddr); // 将结构体清零
localaddr.sin_family = AF_INET; // 告知通讯方式,通常与domain同
localaddr.sin_port = htons(port_); // 端口号:注意转为网络字节序
localaddr.sin_addr.s_addr = ip_.empty() ? INADDR_ANY : inet_addr(ip_.c_str()); // IP:点分十进制->四字节+网络字节序
// 2.2、绑定:
if (bind(sock_, (struct sockaddr *)&localaddr, sizeof localaddr) < 0) // 注意对localaddr类型转换
{
logMessage(ERROR, "%d:%s", errno, strerror(errno));
exit(2);
}
logMessage(DEBUG, "绑定成功,初始化服务器完成!");
return true;
}
// 2、测试二:演示指令执行
// 启动服务器
void Start()
{
// 1、作为一款网络服务器,永远不退出地在接收客服端通过网络传来的请求
while (true)
{
// 1.1、准备工作:
// a、用于后续从网络中读取客户端的IP、端口号
struct sockaddr_in clientaddr;
bzero(&clientaddr, sizeof clientaddr);
socklen_t len = sizeof clientaddr; // 输入输出型参数:输入时传递的是clientaddr当前定义的大小,使用是会输出实际大小
// b、用于recvfrom时存储数据
char server_buffer[SIZE] = "0";
// c、用于业务处理时,记录当前发送消息的客户端:ip-prot(unordered_map的key值)
char key[64] = "0";
// 1.2、读取数据:
ssize_t s = recvfrom(sock_, server_buffer, sizeof(server_buffer) - 1, 0, (struct sockaddr *)&clientaddr, &len); // 注意对clientaddr类型转换
if (s > 0) // 1.3、读取成功,进行业务处理
{
server_buffer[s] = 0;
// a-1、获取客户端端口号、IP:因为是从网络中获取,这里本地显示时需要转换字节序和风格
uint16_t client_port = ntohs(clientaddr.sin_port);
std::string client_ip = inet_ntoa(clientaddr.sin_addr);
// a-2:检测本次发送消息的客户端是否存在_users中,若无,则需要添加进来
snprintf(key, sizeof key, "[%s-%d]", client_ip.c_str(), client_port);
logMessage(DEBUG, "key:%s send a massage to server.", key);
auto ifexist = _users.find(key); // iterator find ( const key_type& k );
if(ifexist ==_users.end())// unordered_map::end
{
// 说明_users中没有记录该客户端,需要插入
_users.insert({key, clientaddr});
logMessage(DEBUG, "add a new user: %s .", key);
}
//
}
// 2、响应:将处理好的结果返回。
// c-1:遍历_users,挨个推送消息
for (auto &iter : _users)
{
// 传回格式: ip-prot# XXXXXX ,例如:[127.0.0.1-8080]# 你好!
std::string respond = key;
respond += "# ";
respond += server_buffer;
logMessage(DEBUG,"push message to: %s .",key);
sendto(sock_, respond.c_str(), respond.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second));
}
}
}
private:
uint16_t port_; // 端口号:16位的整数
std::string ip_; // IP地址:点分十进制字符串风格
int sock_; // 通讯时的套接字:需要供多处使用
std::unordered_map<std::string, struct sockaddr_in> _users;
//用于记录连接上服务器的所有客户端:[ip-prot,sockaddr结构体],即[字符串信息显示的IP和端口号,实际用于获取IP和端口号的结构]
};
#endif
说明:引入了多线程,将读取数据和接收数据分开处理。(线程部分这里直接使用了之前在生产者消费者模式中写过的封装类)。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "thread.hpp"
#include "log.hpp"
#define SIZE 1024
//演示三:简易版聊天系统(多线程模式)
//服务器IP、Port:
//由于客户端我们没有封装,而这两个参数在线程中需要,一种方法是写类进行处理,另一种方法是定义成全局函数
//不存在线程安全问题,因为我们只是使用它,不是对它进行修改。(将sock当作一个文件,UDP是全双工的,可以同时进行收发而不受干扰)
in_addr_t server_ip;
uint16_t server_port;
// 使用手册:当命令行输入不正确时,可提示正确使用信息
void Usage(std::string proc)
{
std::cout << "\nUsage:" << proc << " server_ip server_port\n"
<< std::endl;
}
// 用于向服务端发送数据消息
void *udpSend(void *pdata)
{
// 1、通信前的准备:
// a、获取args参数:
int sock = *(int*)(((ThreadData *)pdata)->_args);
// b、获取服务端的IP、端口号
struct sockaddr_in serveraddr;
bzero(&serveraddr, sizeof serveraddr);
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = server_port; // 这里已经在main函数中做了转换
serveraddr.sin_addr.s_addr = server_ip;
// 2、客户端从显示器中输入数据,将其发送给服务端:sendto(循环式发送)
while (true)
{
string message;
//std::cout << "client-请输入# ";
std::cerr << "client-请输入# ";
std::getline(std::cin, message);
if (message == "quit") // string类中有operator==,建议访问官网查阅
break;
// 当client首次发送消息给服务器的时候,OS会自动给client bindIP和PORT.
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&serveraddr, sizeof serveraddr);
}
}
// 用于从服务端接收数据信息
void* udpRecv(void* pdata)
{
// 1、通信前的准备:
// a、获取args参数
int sock = *(int*)(((ThreadData*)pdata)->_args);
// 2、recvfrom循环式接收服务器发送过来的数据
while(true)
{
char client_buffer[SIZE]; // 用于存储数据
struct sockaddr_in temp; // 临时变量:用于recvfrom中参数(为保证该函数成功使用)
memset(&temp, 0, sizeof temp);
socklen_t len = sizeof temp;
ssize_t s = recvfrom(sock, client_buffer, sizeof client_buffer, 0, (struct sockaddr *)&temp, &len);
if (s > 0) // 接收到实际数据
{
client_buffer[s] = 0;
std::cout << client_buffer << std::endl;
}
}
}
// 启动方式:udp_client server_ip server_port
int main(int argc, char **argv)
{
// 1、检测命令行参数是否输入正确
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
//获取服务器的IP、套接字,将其转换为网络传送需要的套接字
server_ip = inet_addr(argv[1]);//点分十进制字符串风格--->网络字节序+4字节
server_port = htons(atoi(argv[2]));//字符型--->网络字节序+整型
// 2、创建套接字
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) // 创建失败
{
std::cerr << "socket error" << std::endl;
exit(2);
}
logMessage(DEBUG, "socket succes. sock: %d", sock);
// 3、创建两个线程,分别用于管理客户端的[发送消息]和[接收消息]
std::unique_ptr<Thread> sender(new Thread(1,udpSend,(void*)&sock));
std::unique_ptr<Thread> recever(new Thread(2,udpRecv,(void*)&sock));
sender->start();
recever->start();
sender->join();
recever->join();
// 4、结束:关闭套接字
close(sock);
return 0;
}
多个客户端时的结果演示:
PS:这些演示都是在同一主机下进行,若要网络通信,收发消息的主机不同,可将客户端程序传送给其它主机,让其拿着服务器的端口号和IP地址运行程序即可。(需要开放服务器所在主机的端口号)
实则上述客户端部分还可以进行封装处理。
#pragma once
#include
#include
#include
using namespace std;
typedef void* (*func_t)(void*);//函数指针:此处用于线程表示线程的执行函数
//args传参设置:设置成类,增加args传参选择
class ThreadData
{
public:
string _name;//对应线程名称
void* _args;//对应线程回调函数中args参数
};
class Thread
{
public:
Thread(int inode, func_t rountine, void* args)
:_routine_func(rountine)//注意:这里线程的执行函数、参数args都是需要通过外部传入的
{
char buffer[64]="";
snprintf(buffer, sizeof(buffer), "thread-%d",inode);
_name = buffer;
_tdata._args = args;
_tdata._name = _name;
}
~Thread()
{}
void start()//启动线程:用于创建线程,构造函数只是做了线程名称、ID等各参数设置,实则并未真正创建出线程
{
pthread_create(&_tid, nullptr, _routine_func, (void*)&_tdata);
}
void join()//终止线程
{
pthread_join(_tid, nullptr);
}
private:
string _name;//线程名
pthread_t _tid;//线程ID
func_t _routine_func;//线程的执行函数
ThreadData _tdata;//传入回调函数的参数(这里做了封装)
};