【ONE·Linux || 网络基础(一)】
总言
主要内容:简述网络传输流程(TCP/IP五层模式概念认知,Mac地址、端口号、网络字节序等),演示socke套接字编程(UDP模式)。
文章目录
- 总言
- 1、基础简述
- 1.1、计算机网络背景
- 1.2、认识网络协议(TCP/IP五层结构模型)
- 1.3、网络传输的基本流程
- 1.3.1、TCP/IP通讯过程(封装和分用、ifconfig)
- 1.3.2、Mac地址、IP地址
- 1.3.3、端口号
- 1.4、其它
- 1.4.1、TCP协议和UDP协议
- 1.4.2、网络字节序(字节序转换的函数)
- 2、socket编程接口
- 2.1、sockaddr结构
- 3、基于套接字的UDP网络程序
- 3.0、log.hpp日志
- 3.1、udp_server.hpp(服务器)
- 3.1.1、成员变量与构造、析构函数
- 3.1.2、初始化服务器:bool initServer()
- 3.1.2.1、socket
- 3.1.2.2、bind、bzero、htons、inet_addr
- 3.1.3、启动服务器:void Start()
- 3.1.3.1、recvfrom、inet_ntoa
- 3.1.3.2、sendto
- 3.1.4、该部分整体框架(测试一:echo版服务器)
- 3.1.4.1、相关代码
- 3.1.4.2、验证
- 3.2、服务端和客户端
- 3.2.1、udp_server.cc
- 3.2.2、udp_client.cc
- 3.3、测试二:指令执行
- 3.3.1、相关函数介绍:popen、strcasestr
- 3.3.2、代码实现与结果演示
- 3.4、测试三:多线程版简易聊天系统
- 3.4.1、server.hpp
- 3.4.2、udp_client.cc
- 3.4.3、演示结果、thread.hpp
- 3.4.3.1、演示结果
- 3.4.3.2、thread.hpp
1、基础简述
1.1、计算机网络背景
1)、网络发展说明
独立模式: 计算机之间相互独立;
网络互联: 多台计算机连接在一起, 完成数据共享;
局域网LAN: 计算机数量更多了, 通过交换机和路由器连接在一起。
广域网WAN: 将远隔千里的计算机都连在一起;
PS:所谓 “局域网” 和 “广域网” 只是一个相对的概念.。
1.2、认识网络协议(TCP/IP五层结构模型)
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)工作在物理层。 |
网络编程主要是针对应用层。
1.3、网络传输的基本流程
1.3.1、TCP/IP通讯过程(封装和分用、ifconfig)
1)、总体呈现
2)、TCP/IP通讯过程
同一个网段内的两台主机可以进行文件传输(局域网中两台主机是可以直接通信的)。虽然在通讯双方看来是彼此之间的通信(应用层->应用层),实际其经过了一个自顶向下,又自底向上的过程(应用层到底层,底层到应用层)。
3)、数据包封装和分用
不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报 (datagram),在链路层叫做帧(frame)。
应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装(Encapsulation)。首部信息中包含了一些类似于首部有多长,载荷(payload)有多长,上层协议是什么等信息。数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,根据首部中的 “上层协议字段” 将数据交给对应的上层协议处理。
数据分选细节如下:
1.3.2、Mac地址、IP地址
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.3.3、端口号
1)、问题引入
问题:对当前主机而已,把自身的数据送到对方的主机,是最终目的吗?
回答:并不是。真正的网络通信过程,本质其实是进程间通信。(例如:客户端进程、服务器进程)。
将数据在主机间转发仅仅是用于完成通信的手段,对方主机接收到数据之后,需要将数据交付给指定的进程! 而OS中可有多个进程同时运行,如何确定将数据交给哪一个进程?因此,引入端口号。
2)、概念与相关说明
端口号(port):传输层协议的内容,是一个2字节16比特位的整数。用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理。
端口号具有唯一性,一个端口号只能被一个进程占用。IP地址 + 端口号
,能够标识网络上的某一台主机的某一个进程。
说明一:“端口号” 和 “进程ID”
①二者都具唯一性,但一个是网络模块,一个是进程管理模块。不是不能互相只用一个,但这样一来会将两个不同模块之间关联起来,不如各自搞各自的一套执行方案,实现数据解耦。
②并非所有进程都需要网络通信(端口号),一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定。
说明二:源端口号和目的端口号
①传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号。就是在描述 “数据是谁发的, 要发给谁”(谁发数据,谁就是源端口号;谁接收数据,谁就是目的端口号)。
1.4、其它
1.4.1、TCP协议和UDP协议
1)、各自特点
TCP协议:Transmission Control Protocol 传输控制协议。
传输层协议
有连接
可靠传输
面向字节流
(关于可靠、不可靠:不能肤浅的字面理解。如数据丢包之类,有些场景丢包影响并不大,而可靠的背后代表着为了数据安全该协议会做大量处理工作,增加工作量和维护成本。)
UDP协议:User Datagram Protocol 用户数据报协议。
传输层协议
无连接
不可靠传输
面向数据报
1.4.2、网络字节序(字节序转换的函数)
1)、基本说明
内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据。如果当前发送主机是小端,就需要先将数据转成大端,否则就忽略直接发送即可。
2)、用于做网络字节序和主机字节序转换的函数
为使网络程序具有可移植性、使同样的C代码在大端和小端计算机上编译后都能正常运行。可以调用以下库函数做网络字节序和主机字节序的转换。
NAMEhtonl, htons, ntohl, ntohs - convert values between host and network byte orderSYNOPSIS#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。hton:主机传网络;ntoh:网络传主机。
l
表示32位长整数(unsigned integer),s
表示16位短整数(unsigned short integer)。
例如:htonl
表示将32位的长整数从主机字节序转换为网络字节序。实际场景举例:将IP地址转换后准备发送。
DESCRIPTIONThe 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.
2、socket编程接口
以下内容为小节3、4涉及,这里只是将其总结性拎出方便查阅。
2.1、sockaddr结构
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 */
3、基于套接字的UDP网络程序
3.0、log.hpp日志
#pragma once#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>// 日志级别
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4const char *gLevelMap[] = {"DEBUG","NORMAL","WARNING","ERROR","FATAL"
};// 完整的日志功能,至少有 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)//const char *format, ... 可变参数
{
#ifndef DEBUG_SHOWif(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 == nullptrprintf("%s%s\n", stdBuffer, logBuffer);
}
3.1、udp_server.hpp(服务器)
3.1.1、成员变量与构造、析构函数
以下为服务器基本框架搭建:使用时,只需要在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_; // 通讯时的套接字:需要供多处使用
};
3.1.2、初始化服务器:bool initServer()
3.1.2.1、socket
1)、相关函数介绍
man socket
可查看该函数,注意其包含的头文件。
NAMEsocket - create an endpoint for communicationSYNOPSIS#include <sys/types.h> /* See NOTES */#include <sys/socket.h>int socket(int domain, int type, int protocol);DESCRIPTIONsocket() 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 whichwill be used for communication. These families are defined in <sys/socket.h>. The currentlyunderstood formats include:Name Purpose Man pageAF_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. Currentlydefined 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 maximumlength).
返回值
:若成功,则返回一个file descriptor
,即文件描述符(指向对应的套接字)。若失败则返回-1,并设置错误码。
RETURN VALUEOn success, a file descriptor for the new socket is returned. On error, -1 is returned, anderrno 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_);
3.1.2.2、bind、bzero、htons、inet_addr
1)、相关函数介绍
bind
man bind
:当使用socket()
创建套接字时,它存在于命名空间(地址族)中,但没有地址分配给它。bind()
将addr指定的地址分配给文件描述符引用的套接字。
NAMEbind - bind a name to a socketSYNOPSIS#include <sys/types.h> /* See NOTES */#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);DESCRIPTIONWhen a socket is created with socket(2), it exists in a name space (address family) but has no addressassigned to it. bind() assigns the address specified by addr to the socket referred to by the file descriptorsockfd. 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 VALUEOn 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);
NAMEbzero - write zero-valued bytesSYNOPSIS#include <strings.h>void bzero(void *s, size_t n);DESCRIPTIONThe bzero() function sets the first n bytes of the area starting at s to zero (bytes containing '\0').RETURN VALUENone.
htons、inet_addr
htons
:网络字节序转换的函数。
说明:这里主要用于port端口号(大小通常为16位)。服务器的IP和端口未来也是要发送给客户端主机,因此要先将数据发送到网络中,而发送到网络时涉及网络字节序。
SYNOPSIS#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong);uint16_t htons(uint16_t hostshort);DESCRIPTIONThe htonl() function converts the unsigned integer hostlong from host byte order to network byteorder.The htons() function converts the unsigned short integer hostshort from host byte order to networkbyte 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 <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>in_addr_t inet_addr(const char *cp);DESCRIPTIONThe inet_addr() function converts the Internet host address cp from IPv4 numbers-and-dots notationinto binary data in network byte order. If the input is invalid, INADDR_NONE (usually -1) isreturned. 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 wayto 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)
3.1.3、启动服务器:void Start()
3.1.3.1、recvfrom、inet_ntoa
1)、相关函数介绍
NAMErecv, recvfrom, recvmsg - receive a message from a socketSYNOPSIS#include <sys/types.h>#include <sys/socket.h>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);DESCRIPTIONThe recvfrom() and recvmsg() calls are used to receive messages from a socket, and may be used toreceive 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 sourceaddress is filled in. When src_addr is NULL, nothing is filled in; in this case, addrlen is notused, and should also be NULL. The argument addrlen is a value-result argument, which the callershould initialize before the call to the size of the buffer associated with src_addr, and modifiedon return to indicate the actual size of the source address. The returned address is truncated ifthe buffer provided is too small; in this case, addrlen will return a value greater than was sup‐plied to the call.
返回值:
RETURN VALUEThese calls return the number of bytes received, or -1 if an error occurred. In the event of anerror, errno is set to indicate the error. The return value will be 0 when the peer has performedan 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、获取客户端端口号、IPuint16_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 <arpa/inet.h>uint32_t ntohl(uint32_t netlong);uint16_t ntohs(uint16_t netshort);DESCRIPTIONThe ntohl() function converts the unsigned integer netlong from network byte order to host byteorder.The ntohs() function converts the unsigned short integer netshort from network byte order to hostbyte order.
inet_ntoa
:同理,对IP地址使用。①网络字节序到服务器字节序;②4字节到点分十进制风格;
SYNOPSIS#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>char *inet_ntoa(struct in_addr in);DESCRIPTIONThe inet_ntoa() function converts the Internet host address in, given in network byte order, to astring in IPv4 dotted-decimal notation. The string is returned in a statically allocated buffer,which subsequent calls will overwrite.
3.1.3.2、sendto
1)、相关函数介绍
NAMEsend, sendto, sendmsg - send a message on a socketSYNOPSIS#include <sys/types.h>#include <sys/socket.h>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);DESCRIPTIONThe 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 argumentsdest_addr and addrlen are ignored (and the error EISCONN may be returned when they are not NULL and0), and the error ENOTCONN is returned when the socket was not actually connected. Otherwise, theaddress of the target is given by dest_addr with addrlen specifying its size. For sendmsg(), theaddress 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 VALUEOn success, these calls return the number of characters sent. On error, -1 is returned, and errno isset 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);
3.1.4、该部分整体框架(测试一:echo版服务器)
3.1.4.1、相关代码
这里还会随着后续通讯的业务处理需求不断调整改变。
#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <memory>
#include <cstdlib>
#include <cerrno>
#include <cstring>
#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_INETsock_ = 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
3.1.4.2、验证
netstat -anup
:可查看当前存在的所有UDP。
127.0.0.1
:本地环回,client和server发送数据只在本地协议栈中进行数据流动,不会把数据发送到网络中。(通常用于本地网络测试,若使用127.0.0.1可正常通讯,而连接其它网络无法通信,大概率是网络问题)。
3.2、服务端和客户端
3.2.1、udp_server.cc
PS:①云服务器无法bind公网IP,也不建议。②对server服务器来讲,也不推荐bind确定的IP(上述INADDR_ANY
有举例)。
#include "udp_server.hpp"
#include <memory>//使用手册:当命令行输入错误时,可提示正确启动信息
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;
}
3.2.2、udp_client.cc
#include <iostream>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>#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;
}
3.3、测试二:指令执行
3.3.1、相关函数介绍:popen、strcasestr
需求说明: 客户端发送指令数据,服务端接收,执行指令,并将其结果返回给客户端。
1)、popen
如何实现: 从命令行参数接收指令并执行,该操作我们在进程控制中学习过。服务端可创建管道,创建子进程,让子进程从管道中读取指令,进行替换执行相关指令。这里我们介绍一个函数,将上述操作一步到位:popen
。
NAMEpopen, pclose - pipe stream to or from a processSYNOPSIS#include <stdio.h>FILE *popen(const char *command, const char *type);int pclose(FILE *stream);
函数说明: 1、创建管道pipe(),创建子进程fork(),exec进程替换执行相关指令command。2、FILE返回值:将执行结果通过FILE*指针读取。
DESCRIPTIONThe popen() function opens a process by creating a pipe, forking, and invoking the shell. Since a pipe is bydefinition unidirectional, the type argument may specify only reading or writing, not both; the resultingstream is correspondingly read-only or write-only.The command argument is a pointer to a null-terminated string containing a shell command line. This commandis passed to /bin/sh using the -c flag; interpretation, if any, is performed by the shell. The type argumentis 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 theclose-on-exec flag (FD_CLOEXEC) to be set on the underlying file descriptor; see the description of theO_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 closedwith 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 alteredby the command itself. Conversely, reading from a "popened" stream reads the command's standard output, andthe 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 VALUEThe 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 eventof an error, these functions set errnro to indicate the cause of the error.
2)、strcasestr
说明: 为了防止客户端发送来rm
、rmdir
等系列指令,需要进行一层过滤处理。可以使用此函数,其和strstr类似,只是在找字串时,忽略大小写。
NAMEstrstr, strcasestr - locate a substringSYNOPSIS#include <string.h>char *strstr(const char *haystack, const char *needle);#define _GNU_SOURCE /* See feature_test_macros(7) */#include <string.h>char *strcasestr(const char *haystack, const char *needle);DESCRIPTIONThe 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 VALUEThese functions return a pointer to the beginning of the substring, or NULL if the substring is not found.
3.3.2、代码实现与结果演示
实则此部分只需要变动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);}}
演示结果:
3.4、测试三:多线程版简易聊天系统
需求说明: 当某一用户发送消息时,将该消息推送给所有用户。
3.4.1、server.hpp
改动说明:同理,只需要改变void Start()里业务处理部分的内容。注意需要将结果一一传回。
#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unordered_map>
#include <string>
#include <memory>
#include <cstdlib>
#include <cerrno>
#include <cstring>
#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_INETsock_ = 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
3.4.2、udp_client.cc
说明:引入了多线程,将读取数据和接收数据分开处理。(线程部分这里直接使用了之前在生产者消费者模式中写过的封装类)。
#include <iostream>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <memory>
#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;
}
3.4.3、演示结果、thread.hpp
3.4.3.1、演示结果
以下为当个客户端时的结果演示:
多个客户端时的结果演示:
PS:这些演示都是在同一主机下进行,若要网络通信,收发消息的主机不同,可将客户端程序传送给其它主机,让其拿着服务器的端口号和IP地址运行程序即可。(需要开放服务器所在主机的端口号)
3.4.3.2、thread.hpp
实则上述客户端部分还可以进行封装处理。
#pragma once
#include<iostream>
#include<string>
#include<pthread.h>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;//线程IDfunc_t _routine_func;//线程的执行函数ThreadData _tdata;//传入回调函数的参数(这里做了封装)
};
相关文章:

【ONE·Linux || 网络基础(一)】
总言 主要内容:简述网络传输流程(TCP/IP五层模式概念认知,Mac地址、端口号、网络字节序等),演示socke套接字编程(UDP模式)。 文章目录 总言1、基础简述1.1、计算机网络背景1.2、认识网络协议&a…...

Day12力扣打卡
打卡记录 找出满足差值条件的下标 II(双指针维护最大最小) 链接 采用双指针保留间隔 indexDifference 进行遍历,求出慢指针对应一路遍历过来的最大值和最小值。 class Solution { public:vector<int> findIndices(vector<int>…...

SQL注入原理及思路(mysql)
数据库知识 mysql数据库 show database; #列出所有数据库 show tables; #列出所有表名 show columns from 表名; #列出表的列 select * from 表名 #查询数据库中某表的信息 select * from 表名 where 列xx #查询某表中符合列xx的信息 select * from 表名 order by 数字 #用于将…...

vue核心面试题汇总【查缺补漏】
给大家推荐一个实用面试题库 1、前端面试题库 (面试必备) 推荐:★★★★★ 地址:web前端面试题库 很喜欢‘万变不离其宗’这句话,希望在不断的思考和总结中找到Vue中的宗,来解答面试官抛出的…...

使用WebStorm创建和配置TypeScript项目
创建 这里我用的是WebStorm 2019.2.2版本 首先,创建一个空项目 File -> New -> Project->Empty Project生成配置文件 自动配置: 打开终端输入tsc --init,即可自动生成tsconfig.json文件 手动配置: 在项目根目录下新建一…...

vue源码分析(四)——vue 挂载($mount)的详细过程
文章目录 前言一、使用RuntimeCompiler解析$mount的原因二、$mount 解析的详细过程1.解析挂载的#app执行了vm.$mount2. 通过$mount方法执行以下文件的mount方法3. 执行util工具文件夹中的query方法4. 执行query方法后返回$mount方法判断el是否是body5. 判断!options.render&…...

真机环境配置教程
1.下载安装包 https://developers.google.com/android/images 2.刷机教程 Xposed精品连载 | 一篇文章彻底搞定安卓刷机与Root 3.配置root...

新电脑第一次重启后蓝屏
新电脑第一次重启后蓝屏 悲惨事故,远程参加插电第一次开机,按“FNShiftF10”启动cmd窗口输入oobe\bypassnro 回车重启跳过网络连接,设置一个用户名密码设置为空,不设密码确定,进入系统软件操作磁盘操作(磁盘…...

k8s statefulSet 学习笔记
文章目录 缩写: stsweb-sts.yaml创建sts扩缩容金丝雀发布OnDelete 删除时更新 缩写: sts 通过 kubectl api-resources 可以查到: NAMESHORTNAMESAPIVERSIONNAMESPACEDKINDstatefulsetsstsapps/v1trueStatefulSet web-sts.yaml apiVersion: v1 kind: Service met…...

gitlab 通过变量连接自建K8S
services:- docker:19.03.7-dind- golang:1.17.8-alpine3.15- docker:stable stages:- package- build and push docker image- deploy variables:KUBECONFIG: /etc/deploy/config build:tags:- k8simage: golang:1.17.8-alpine3.15stage: package# 只作用在main分支only:- mai…...

LuatOS-SOC接口文档(air780E)--mcu - 封装mcu一些特殊操作
常量 常量 类型 解释 mcu.UART number 外设类型-串口 mcu.I2C number 外设类型-I2C mcu.SPI number 外设类型-SPI mcu.PWM number 外设类型-PWM mcu.GPIO number 外设类型-GPIO mcu.I2S number 外设类型-I2S mcu.LCD number 外设类型-LCD mcu.CAM num…...

第14期 | GPTSecurity周报
GPTSecurity是一个涵盖了前沿学术研究和实践经验分享的社区,集成了生成预训练 Transformer(GPT)、人工智能生成内容(AIGC)以及大型语言模型(LLM)等安全领域应用的知识。在这里,您可以…...

【数据结构】优先级队列
⭐ 作者:小胡_不糊涂 🌱 作者主页:小胡_不糊涂的个人主页 📀 收录专栏:浅谈数据结构 💖 持续更文,关注博主少走弯路,谢谢大家支持 💖 PriorityQueue 1. 什么是优先级队列…...

c语言宏相关高级用法
outline all可变参数宏c语言内置函数1.__typeof__2.__builtin_choose_expr all 记录一些c语言宏相关的高级用法 可变参数宏 c语言内置函数 1.typeof 2.__builtin_choose_expr 语法格式 type __builtin_choose_expr (const_exp, exp1, exp2)解释 这个函数的第一个参数必须…...

新款模块上线实现SIP模块与扩拨电话之间打点与喊话功能 IP矿用电话模块SV-2800VP
新款模块上线实现SIP模块与扩拨电话之间打点与喊话功能 IP矿用电话模块SV-2800VP 一、简介 SV-2800VP系列模块是我司设计研发的一款用于井下的矿用IP音频传输模块,可用此模块打造一套低延迟、高效率、高灵活和多扩展的IP矿用广播对讲系统,亦可对传统煤…...

前端开发---在vue项目中使用openLayers
前端开发之在vue项目中使用openLayers 前言效果图在vue中渲染地图安装ol插件1、调用插件2、 初始话地图3、地图点击事件4、重置坐标5、通过坐标改变视图6、保存坐标点 vue中使用的源码 前言 本篇文章主要讲解openLayers的初步使用,包括渲染地图、获取点坐标、标记点…...

C语言之结构体和共用体详解
目录 结构体 结构体的定义和使用 结构体数组的使用 结构体指针的使用 结构体大小的计算 共用体 共用体的定义和使用 typedef用法详解 enum枚举类型 结构体 结构体的定义和使用 C语言的结构体(Struct)是一种自定义的数据类型,它允许…...

iOS插件
把平时看到或项目用到的一些插件进行整理,文章后面分享一些不错的实例,若你有其它的插件欢迎分享,不断的进行更新; 一:第三方插件 1:基于响应式编程思想的oc 地址:https://github.com/ReactiveCocoa/Rea…...

Maven第四章:配置文件详解
Maven第四章:配置文件详解 前言 本章重点知识:掌握setting.xml配置文件以及pom.xml配置文件 setting.xml配置文件 setting.xml文件用于配置Maven的运行环境,包括本地仓库的位置、镜像仓库的配置、认证信息等。以下是setting.xml文件的详细说明: 文件位置: 全局配置文件:…...

计算机网络基础一
任务背景 由于某些原因,某公司搬迁至新地方,现需要对公司网络环境重新调整规划,申请了一个 B 类 IP 地址 : 172.25.0.0 ,子 网掩码为 255.255.224.0 。需要根据公司部门和电脑数进行子网划分并分配 IP 。公司目前有 6 个部门&am…...

搜维尔科技:Touch触觉式力反馈设备与Touch X力反馈设备对比分析
此2款力反馈为最常用的力反馈设备...

SAP保持系统长时间在线
保持系统长时间在线 保持SAP系统长长时间在线不掉线,通过代码,保持一个页面一直在线,ABAP代码如下: *&---------------------------------------------------------------------* *& Report ZGUI *&----------------------------…...

威联通NAS进阶玩法之使用Docker搭建个人博客教程
Hello大家好,本篇教程主要教大家在威联通的NAS上搭建属于自己的个人博客网站,首先介绍一下我使用的机器,四盘位威联通TS-464C2,搭载四核四线程的N5095处理器,支持4K60帧的输出以及PCIE3.0,可玩性还是非常高的。废话不多…...

模型对象CSS2DObject始终在画布的左上角(问题解决)
写了个简单案例模拟一下这个问题,看下图片 下面看下c2渲染器相关代码部分 this.css2DRenderer new CSS2DRenderer(); this.css2DRenderer.render(this.scene, this.camera); this.css2DRenderer.setSize(width, height); this.css2DRenderer.domElement.style.pos…...

LabVIEW开发基于图像处理的车牌检测系统
LabVIEW开发基于图像处理的车牌检测系统 自动车牌识别的一般步骤是图像采集、去除噪声的预处理、车牌定位、字符分割和字符识别。结果主要取决于所采集图像的质量。在不同照明条件下获得的图像具有不同的结果。在要使用的预处理技术中,必须将彩色图像转换为灰度&am…...

Data Analysis With Python
文章目录 Data Analysis With PythonAnalyzing Numerical Data with NumPyCreating NumPy ArrayNumPy Array SlicingNumPy Array BroadcastingAnalyzing Data Using Pandas In this article, we will discuss how to do data analysis with Python. We will discuss all sorts …...

【Selenium】提高测试爬虫效率:Selenium与多线程的完美结合
前言 使用Selenium 创建多个浏览器,这在自动化操作中非常常见。 而在Python中,使用 Selenium threading 或 Selenium ThreadPoolExecutor 都是很好的实现方法。 应用场景: 创建多个浏览器用于测试或者数据采集;使用Selenium 控…...

ElCLib类解析
OpenCascade 中的 ElCLib 类提供了对基本曲线(例如 2D 和 3D 空间中的二次曲线和直线)进行基本几何计算的函数。它提供与参数化、点评估和曲线参数范围内的定位相关的各种操作和计算。以下是一些需要注意的要点: 点和矢量计算:ElC…...

栈、队列、矩阵的总结
栈的应用 括号匹配 表达式求值(中缀,后缀) 中缀转后缀(机算) 中缀机算 后缀机算 总结 特殊矩阵 对称矩阵的压缩存储 三角矩阵 三对角矩阵 稀疏矩阵的压缩存储...

PCL 半径滤波剔除噪点
目录 一、算法原理二、注意事项三、代码实现一、算法原理 PCL半径滤波是删除在输入的点云一定范围内没有达到足够多领域的所有数据点。通俗的讲:就是以一个点p给定一个范围r,领域点要求的个数为m,r若在这个点的r范围内部的个数大于m则保留,小于m则删除。因此,使用该算法时…...