文章目录
- 0.预备知识
- 0.1套接字
- 0.2TCP/UDP
- 0.3大小端问题
- 1.socket 常见API
- 1.1socket
- 1.2各个接口
- 1.3int bind();
- 1.3网络头文件四件套
- 1.4bzero
- 1.5recvfrom
- 1.6sendto()
- 2.UDP编程
- 2.1服务器编程
- 2.2客户端编程
- 2.3运行测试
- 2.3.1本机通信
- 2.3.2popen
- 2.3.3strcasestr
- 2.3.4回顾C++11智能指针
- 3.linux网络涉及到的协议栈
- 4.三个版本的服务器
- 4.1响应式
- 4.2命令式
- 4.3交互式
- 1.启动程序
- 2.运行结果
0.预备知识
0.1套接字
常见的套接字:
- 域间socket:基于套接字的管道通信/服务端/客户端主客通信
- 原始socket:用来创建工具/应用层跨传输层到网络层/或应用层直接到数据链路层
- 网络socket:跨网络
理论上,是三种应用场景,对应的应该是三套接口! ==》不想设计过多的接口!将所有的接口进行统一;
如果不设计统一的接口,三个接口,参数大部分相似,只有一个参数需要传特定的。为了一个参数的差异设计三“套”接口?不可取!为什么不用void*?网络接口的设计时,C语言还没出现!现在出来了,能改吗?大厦已经建成,地基无法更改!
0.2TCP/UDP
udp可能出现丢包/乱序问题。可靠和不可靠是客观描述这两个协议的特点的,并不是来评价谁好谁坏,一些场景需要可靠传输,一些场景不可靠传输也行,或者不可靠带来的影响可以容忍。可靠性需要策略/编码来维护适合用udp【直播数据派发,信息流推送,抖音爱奇艺类的服务器使用成本更低,如果公司不差钱,也可以用tcp】用udp,其他一律tcp。
0.3大小端问题
- 怎么判断对方发送的数据是大端小端?发送的消息的报头中添加大小端的标识信息,可以吗?显然不可以,你压根不知道这条信息要用大端还是小端的方式接收,又怎么会知道他的报头中是大端还是小端?==》网络规定,所有网络数据,都必须是大端【数据地位放在地址高位】!至于为什么用大端,目前没有可靠依据。猜测是随便定的,毕竟如今大小端存储器还存在争议;一方无法说服另一方;发收信息的大小端转化不用我们维护,接口自动维护;但是对内核属性填充,对一些地址转化需要我们维护 =》学接口
- 内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址. TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节. 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
- 为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
h表示host,n表示network,l表示32位长整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些函数不做转换,将参数原封不动地返
1.socket 常见API
1.1socket
socket 是 Linux 和其他类 Unix 系统上用于创建网络套接字的函数。它是网络编程中的一个基础函数,用于初始化套接字通信。以下是关于 socket 函数的参数、返回值和工作原理的详细描述。
参数
domain:指定通信的协议族。常见的协议族有:
AF_INET:IPv4 网络协议。
AF_INET6:IPv6 网络协议。
AF_UNIX:本地(Unix 域)套接字,用于同一台机器上的进程间通信。
type:指定套接字的类型。常见的套接字类型有:
SOCK_STREAM:提供流式套接字,通常用于 TCP 协议。
SOCK_DGRAM:提供数据报套接字,通常用于 UDP 协议。
SOCK_RAW:提供原始套接字,可以访问底层协议。
protocol:指定使用的特定协议。大多数情况下,可以设置为 0,表示使用默认协议。
返回值
如果成功,socket 函数返回一个非负整数,这个整数就是新创建的套接字的文件描述符。如果出现错误,则返回 -1,并设置全局变量 errno 以指示错误原因。
之前学的文件都是字节流式的,udp是面向数据报的,tcp是面向字节流的。udp不能直接使用文件的接口。
工作原理
socket 函数的工作原理可以大致描述为以下几个步骤:
初始化:根据给定的参数(domain, type, protocol),系统内核会为新套接字分配必要的资源,并初始化套接字的数据结构。
返回文件描述符:成功创建套接字后,系统会返回一个唯一的文件描述符。这个描述符用于后续的网络操作,如绑定地址、监听连接、发送和接收数据等。
错误处理:如果由于某种原因(如资源不足、参数错误等)无法创建套接字,socket 函数会返回 -1,并设置 errno 以指示错误原因。
函数功能
socket 函数的主要功能是创建一个新的套接字,并返回一个与之关联的文件描述符。这个套接字可以用于后续的网络通信操作,如建立连接、发送和接收数据等。它是进行网络编程的基础和起点。
下面是一个简单的示例,展示了如何使用 socket 函数创建一个 TCP 套接字:
c
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main() {
int sockfd;
struct sockaddr_in server_addr;
// 创建 TCP 套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 配置服务器地址信息(这里省略了具体配置)
// ...
// 绑定套接字到指定地址(这里省略了具体绑定操作)
// ...
// 监听连接请求(如果是服务器端代码的话)
// ...
// 接受连接请求或发起连接请求(根据是客户端还是服务器端)
// ...
// 发送和接收数据(根据通信协议进行)
// ...
// 关闭套接字
close(sockfd);
return 0;
}
请注意,这只是一个简单的示例,用于说明如何使用 socket 函数创建套接字。在实际的网络编程中,还需要进行更多的配置和操作,如绑定地址、监听连接、发送和接收数据等。
套接字是啥,socket函数执行完后,创造了什么?
**套接字(Socket)**是网络编程中的一个基本概念,它是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。套接字是应用层与传输层之间的一个抽象层,它把复杂的TCP/IP协议族隐藏在套接字接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
socket函数在编程中用于创建一个新的套接字。当socket函数执行完后,它主要完成了以下操作:
分配文件描述符:操作系统为每一个socket分配一个唯一的文件描述符,这个文件描述符就像是一个“门牌号”,用于后续的网络通信中标识和操作这个socket。
设置套接字属性:根据传递给socket函数的参数(如协议类型、地址族等),操作系统会设置套接字的相应属性。这些属性决定了套接字的行为和特性。
建立通信端点:socket函数实际上在主机上创建了一个通信的端点,这个端点用于后续的绑定(bind)、监听(listen)、连接(connect)等操作。对于服务端套接字,这个端点通常用于监听特定的端口;对于客户端套接字,这个端点则用于发起连接请求。
因此,socket函数执行完后,主要创建了一个具有特定属性和行为的通信端点,并为这个端点分配了一个唯一的文件描述符,以便后续的网络通信操作。
1.2各个接口
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain
Socket. 然而, 各种网络协议的地址格式并不相同
IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16
位端口号和32位IP地址. IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好
处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为
参数
- 服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络!==》把端口号从主机字节序–》网络字节序
- “192.168.110.132” -> 点分十进制字符串风格的IP地址
每一个区域取值范围是[0-255]即一个字节能表示的大小
理论上,表示一个IP地址,其实4字节就够了!
点分十进制字符串风格的IP地址易于阅读 <-> 4字节的二进制才是网络发送格式;==》转换:服务器向网络发送数据:把点分十进制字符串风格的IP地址转换为4字节主机序列再转换为4字节网络序列:有一套接口,可以一次帮我们做完这两件事情, 宏的使用:不暴露服务器的ip,让服务器在工作过程中,可以从任意IP中获取数据。
一台计算机/服务器,他可能有多张网卡,每张网卡可能都配有不同的IP,如果明确的在服务器端绑定了某一个具体IP,服务器就只能收到来自于具体IP的消息。如果采用ADDR_ANY,意思就是告诉操作系统,凡是发给这台主机上指定端口的所有的数据都给我。如果绑定具体IP,就只把客户端的目标ip是这个具体IP的客户端的报文给服务器。如果有特定需求,设置为特定的ip。即一开始设置ip为空串,此时ip执行三目后是ADDR_ANY;如果一开始是指定的ip。执行三目时就是指定的ip。
哪一套接口?
这些函数是用于处理IPv4地址的常用函数,它们在Linux和许多其他Unix-like系统中都有提供。下面我将逐个解释这些函数的参数、返回值、工作原理和功能。
- inet_aton(const char *cp, struct in_addr *inp)
参数:
cp:一个指向以点分十进制格式(如"192.168.1.1")表示的IPv4地址字符串的指针。
inp:一个指向struct in_addr的指针,用于存储转换后的地址。
返回值:
成功时返回非零值。
失败时返回零。
工作原理:
将点分十进制格式的字符串转换为二进制格式,并存储在inp指向的struct in_addr中。
功能:
将字符串形式的IPv4地址转换为二进制形式。
- inet_addr(const char *cp)
参数:
cp:一个指向以点分十进制格式表示的IPv4地址字符串的指针。
返回值:
成功时返回转换后的32位IPv4地址(in_addr_t类型)。
失败时返回INADDR_NONE(通常为-1)。
工作原理:
直接将点分十进制格式的字符串转换为32位整数(IPv4地址)。
功能:
将字符串形式的IPv4地址转换为32位整数形式。
- inet_network(const char *cp)
参数:
cp:一个指向网络地址字符串的指针(通常是以点分十进制格式表示的IPv4地址)。
返回值:
成功时返回转换后的网络地址(in_addr_t类型)。
失败时返回-1。
工作原理:
类似于inet_addr,但主要用于处理网络地址(有时处理时忽略主机部分)。
功能:
将字符串形式的网络地址转换为整数形式。
- inet_ntoa(struct in_addr in)
参数:
in:一个struct in_addr,包含要转换的二进制IPv4地址。
返回值:
返回一个指向以点分十进制格式表示的IPv4地址字符串的指针。该字符串通常是一个静态缓冲区,所以不应该被修改。
工作原理:
将二进制格式的IPv4地址转换为点分十进制格式的字符串。
功能:
将二进制形式的IPv4地址转换为字符串形式。
- inet_makeaddr(int net, int host)
参数:
net:网络部分的地址(通常是一个整数)。
host:主机部分的地址(通常是一个整数)。
返回值:
返回一个struct in_addr,其中包含了由net和host组合而成的IPv4地址。
工作原理:
将网络部分和主机部分组合成一个完整的IPv4地址。
功能:
根据网络部分和主机部分创建IPv4地址。
- inet_lnaof(struct in_addr in)
参数:
in:一个struct in_addr,包含要提取主机部分的IPv4地址。
返回值:
返回IPv4地址中的主机部分(in_addr_t类型)。
工作原理:
从IPv4地址中提取主机部分。
功能:
获取IPv4地址中的主机部分。
- inet_netof(struct in_addr in)
参数:
in:一个struct in_addr,包含要提取网络部分的IPv4地址。
返回值:
返回IPv4地址中的网络部分(in_addr_t类型)。
工作原理:
从IPv4地址中提取网络部分。
功能:
获取IPv4地址中的网络部分。
这些函数提供了在字符串和二进制格式之间转换IPv4地址的功能,以及从IPv4地址中提取特定部分(如网络部分或主机部分)的功能。它们在网络编程中特别有用,尤其是在处理套接字和IP地址时。然而,对于IPv6地址,需要使用不同的函数集(如inet_pton和inet_ntop)。
1.3int bind();
bind 函数在 Linux 系统中用于将一个套接字绑定到一个特定的地址和端口上。这是网络编程中的一个关键步骤,尤其是在服务器端编程中。以下是关于 bind 函数的参数、返回值、工作原理和功能的详细解释:
参数
int sockfd:
这是套接字文件描述符,由 socket 函数返回。
*const struct sockaddr addr:
这是一个指向 sockaddr 结构的指针,该结构包含了套接字应该绑定的地址和端口信息。这个地址通常是 sockaddr_in(用于 IPv4)或 sockaddr_in6(用于 IPv6)类型的。
socklen_t addrlen:
这是 addr 参数所指向的结构的长度,通常以字节为单位。
返回值
如果成功,bind 函数返回 0。
如果失败,返回 -1,并设置全局变量 errno 以指示错误原因。
工作原理
bind 函数的工作原理可以简单描述为以下步骤:
验证套接字:
bind 首先检查 sockfd 是否是一个有效的套接字文件描述符。
验证地址:
函数会检查 addr 参数指向的地址是否合法,并且是否与 sockfd 对应的套接字类型兼容(例如,TCP 或 UDP)。
绑定操作:
如果地址和套接字都有效,内核会将套接字绑定到指定的地址和端口上。如果端口已经被其他套接字使用,bind 通常会失败。
更新套接字状态:
一旦绑定成功,套接字的状态会更新为已绑定状态。
函数功能
bind 函数的主要功能是将套接字与特定的网络地址(IP 地址)和端口号关联起来。在服务器端编程中,这一步是必不可少的,因为服务器需要监听一个特定的端口来接收客户端的连接请求。对于客户端套接字,bind 通常不是必需的,因为客户端套接字通常会在连接时自动绑定到一个可用的本地端口。
但是,在某些情况下,客户端可能也想要绑定到一个特定的端口(例如,当使用 UDP 进行通信时),这时就可以使用 bind 函数。
需要注意的是,bind 函数只是将套接字绑定到地址和端口,并不会开始监听连接(对于 TCP 套接字来说,需要使用 listen 函数)。此外,bind 并不保证端口一定是可用的;如果端口已经被其他进程占用,bind 会失败。
1.3网络头文件四件套
1.4bzero
在Linux系统中,bzero函数用于将内存块的前n个字节设置为零。这个函数在strings.h头文件中声明,并且通常用于初始化内存区域或清除内存中的数据。
参数
*void s:
这是一个指向要清零的内存块的指针。你可以传递任何类型的指针给这个函数,因为它只是将内存字节设置为零,并不关心内存块的内容类型。
size_t n:
这是要清零的字节数。这个参数告诉bzero函数应该操作多少字节的内存。
返回值
bzero函数没有返回值(即返回类型为void),它仅仅执行清零操作。
工作原理
bzero函数的工作原理相对简单:
它获取传递给它的内存地址和要清零的字节数。
然后,它遍历这块内存,将每个字节设置为零。
这个过程是低级的,并且直接在内存上进行操作,不涉及任何高级数据结构或对象。
函数功能
bzero函数的主要功能是清除内存块中的数据,将其设置为零。这在多种场景下都很有用,比如:
初始化内存:在分配内存后,你可能想要将这块内存初始化为一个已知的状态(在这种情况下是零),以避免使用未初始化的数据。
清除敏感数据:如果你在处理敏感信息(如密码或密钥),在处理完成后,你可能想要清除这些数据以确保它们不会残留在内存中。
需要注意的是,尽管bzero函数在许多系统上可用,但它并不是C语言标准库的一部分。因此,如果你的代码需要在多个平台上运行,或者你需要一个更标准的解决方案,你可能会想要使用memset函数,它提供了与bzero相似的功能,并且是C语言标准库的一部分。
使用memset函数的示例:
c
#include <string.h>
void *buffer = malloc(100); // 分配内存
if (buffer != NULL) {
memset(buffer, 0, 100); // 使用memset清零内存
// ... 使用buffer ...
free(buffer); // 释放内存
}
1.5recvfrom
在Linux中,recv(), recvfrom(), 和 recvmsg() 是用于从套接字接收数据的函数。这些函数在网络编程中非常有用,尤其是在TCP/IP和UDP协议中。下面是对这三个函数的参数、返回值、工作原理和功能的详细解释。
- recv(int sockfd, void *buf, size_t len, int flags);
参数:
sockfd:套接字文件描述符,即要接收数据的套接字的标识符。
buf:指向一个缓冲区的指针,该缓冲区用于存储接收到的数据。
len:缓冲区buf的大小,即最多可以接收多少字节的数据。
flags:控制接收操作行为的标志。常见的标志有MSG_PEEK(查看数据但不从队列中移除)和MSG_DONTWAIT(非阻塞接收)。
返回值:
成功时返回接收到的字节数。
失败时返回-1,并设置errno以指示错误原因。
工作原理:
从sockfd指定的套接字接收数据,并将数据存储在buf指向的缓冲区中。
接收的字节数最多为len个字节。
根据flags指定的标志,可能会影响接收操作的行为。
功能:
从指定的套接字接收数据。
2. recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
参数:
sockfd:套接字文件描述符。
buf和len:与recv()相同,用于存储接收到的数据。
flags:与recv()相同,控制接收操作行为的标志。
src_addr:指向一个sockaddr结构的指针,用于存储发送方的地址信息。如果不需要地址信息,可以设置为NULL。
addrlen:指向一个整数的指针,用于存储src_addr结构的大小。
返回值:
成功时返回接收到的字节数。
失败时返回-1,并设置errno。
工作原理:
类似于recv(),但还提供了发送方的地址信息。
功能:
从指定的套接字接收数据,并获取发送方的地址信息。这通常用于UDP套接字,因为UDP是无连接的,需要知道每个数据包的来源。
3. recvmsg(int sockfd, struct msghdr *msg, int flags);
参数:
sockfd:套接字文件描述符。
msg:指向一个msghdr结构的指针,该结构包含了接收操作所需的各种信息,如缓冲区、控制消息等。
flags:控制接收操作行为的标志。
返回值:
成功时返回接收到的字节数。
失败时返回-1,并设置errno。
工作原理:
使用msghdr结构提供的信息来接收数据。这个结构提供了更多的灵活性和控制,例如可以接收多个数据块或处理控制消息。
功能:
提供了更高级别的接收功能,允许更复杂的操作和控制。
这三个函数在网络编程中都有广泛的应用,可以根据具体的需求选择使用哪个函数。对于简单的TCP接收操作,通常使用recv()就足够了。对于需要获取发送方地址的UDP操作,recvfrom()更为合适。而recvmsg()则提供了更多的灵活性和控制选项,适合更复杂的场景。
recvfrom的最后一个参数
在 recvfrom 函数的上下文中,最后一个参数 socklen_t *addrlen 是一个输入/输出型参数。这意味着在调用 recvfrom 之前,你需要为它提供一个初始值,而在函数返回后,这个值可能会被修改。
具体来说:
输入:在调用 recvfrom 之前,你需要将 addrlen 设置为 src_addr 指向的 sockaddr 结构体的预期大小。这告诉 recvfrom 函数有多少空间可用来存储地址信息。
输出:在 recvfrom 调用返回后,addrlen 可能会被修改为实际存储在 src_addr 中的地址信息的字节数。这是因为某些地址结构可能包含可选字段,而这些字段可能并不总是被填充,或者地址家族可能使用不同大小的结构体。因此,通过更新 addrlen,recvfrom 可以告诉调用者实际返回了多少地址信息。
这样,addrlen 参数既用于告诉 recvfrom 你有多少空间来存储地址信息,也用于在函数返回后告知你实际存储了多少信息。
因此,在编写代码时,你需要确保:
为 addrlen 提供一个初始值,该值至少应等于你期望存储的地址结构体的最大可能大小(例如,对于 IPv4 地址,这通常是 sizeof(struct sockaddr_in))。
检查 recvfrom 返回后 addrlen 的值,以确保你正确地解释了返回的地址信息。
下面是一个简单的示例,展示了如何使用 addrlen 参数:
c
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <stdio.h>
// ... 省略其他代码,如套接字创建和绑定 ...
char buffer[1024];
struct sockaddr_in client_addr;
socklen_t client_addrlen = sizeof(client_addr); // 初始化 addrlen
ssize_t bytes_received = recvfrom(sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr *)&client_addr, &client_addrlen);
if (bytes_received == -1) {
perror("recvfrom");
// 错误处理
} else {
// 成功处理接收到的数据
// 可以使用 client_addrlen 和 client_addr 来获取客户端的地址信息
}
在这个例子中,client_addrlen 被初始化为 sizeof(client_addr),然后传递给 recvfrom。在 recvfrom 返回后,client_addrlen 可能已经改变,反映了实际存储在 client_addr 中的地址信息的长度。
1.6sendto()
send, sendto, 和 sendmsg 是 Linux 以及其他 Unix-like 系统中用于网络编程的函数,用于向套接字发送数据。以下是这些函数的参数、返回值、工作原理以及功能的简要说明。
send 函数
参数:
int sockfd:套接字文件描述符。
const void *buf:指向要发送数据的缓冲区的指针。
size_t len:要发送的数据的长度(以字节为单位)。
int flags:控制发送操作的标志(如 MSG_DONTWAIT)。
返回值:
成功时返回发送的字节数;失败时返回 -1 并设置 errno。
工作原理:
send 函数将数据从缓冲区 buf 发送到与 sockfd 关联的套接字。数据被发送到与套接字关联的对端,可能是另一个进程或另一台机器上的进程。
功能:
用于向已连接的套接字发送数据。
sendto 函数
参数:
int sockfd:套接字文件描述符。
const void *buf:指向要发送数据的缓冲区的指针。
size_t len:要发送的数据的长度(以字节为单位)。
int flags:控制发送操作的标志。
const struct sockaddr *dest_addr:指向目的地址结构的指针。
socklen_t addrlen:目的地址结构的长度。
返回值:
成功时返回发送的字节数;失败时返回 -1 并设置 errno。
工作原理:
sendto 函数类似于 send,但它允许你指定一个目的地址,通常用于无连接套接字(如 UDP 套接字)。
功能:
用于向指定地址发送数据,常用于 UDP 套接字。
后两个参数:
在sendto函数的原型中,最后两个参数分别是指向sockaddr结构体的指针dest_addr和该结构体的大小addrlen。这两个参数一起指定了接收数据的目的地址。
const struct sockaddr *dest_addr:
这是一个指向sockaddr结构体(或其任何兼容类型,如sockaddr_in用于IPv4或sockaddr_in6用于IPv6)的指针。
这个结构体包含了目标地址的信息,比如IP地址和端口号。
当你想要发送数据到特定的地址时,你需要设置这个参数。如果你想要发送数据到之前已经通过connect函数连接过的地址,那么这个参数可以设置为NULL,并且addrlen参数也应该设置为0。
socklen_t addrlen:
这是一个socklen_t类型的值,表示dest_addr指针指向的sockaddr结构体的大小。
这个值告诉sendto函数dest_addr指向的结构体有多大,以便正确地解析地址信息。
对于IPv4地址,如果你使用sockaddr_in结构体,那么addrlen通常设置为sizeof(struct sockaddr_in)。对于IPv6地址,如果你使用sockaddr_in6结构体,那么addrlen通常设置为sizeof(struct sockaddr_in6)。
这两个参数一起,使得sendto函数能够知道数据应该发送到哪个地址。这是UDP编程中常见的做法,因为UDP是无连接的,每次发送数据都需要指定目标地址。而在TCP编程中,通常会在建立连接(通过connect函数)后使用send函数发送数据,此时就不需要指定目标地址。
sendmsg 函数
参数:
int sockfd:套接字文件描述符。
const struct msghdr *msg:指向 msghdr 结构体的指针,该结构体包含发送消息所需的信息(如数据缓冲区、控制消息等)。
int flags:控制发送操作的标志。
返回值:
成功时返回发送的字节数;失败时返回 -1 并设置 errno。
工作原理:
sendmsg 函数使用 msghdr 结构体来指定发送的数据、目标地址以及可能的控制信息。这允许更复杂的发送操作,包括附加的辅助数据(如文件描述符)。
功能:
用于发送复杂消息,包括数据和可选的控制信息。
注意事项
对于 TCP 套接字,send 和 sendto 可能会阻塞,直到所有请求的数据都被发送或者发生错误。可以使用 MSG_DONTWAIT 标志来使它们变为非阻塞。
对于 UDP 套接字,sendto 是最常用的函数,因为它允许你指定每个数据包的目标地址。
sendmsg 提供了更高的灵活性,但通常只在需要发送复杂消息或控制信息时才使用。
在使用这些函数时,你还需要注意错误处理,检查 errno 以了解失败的具体原因。
2.UDP编程
2.1服务器编程
网络服务器永不退出:服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在 除非挂掉。对于这种情况:慎重使用内存 防止内存泄漏
sudo netstat -anup
netstat -nup 是一个在 Unix 和 Linux 系统中常用的命令,用于显示网络状态信息。这个命令组合了多个选项来提供特定的输出。
下面是 netstat -nup 命令中每个选项的解释:
-n:这个选项告诉 netstat 以数字形式显示地址和端口号,而不是尝试解析主机名、服务名等。这可以加快命令的执行速度,尤其是在网络连接不稳定或主机名解析服务不可用的情况下。
-u:这个选项指定 netstat 只显示 UDP(用户数据报协议)相关的连接和监听端口。UDP 是一种无连接的协议,常用于不需要建立持久连接的应用,如 DNS 查询、VoIP 通信等。
-p:这个选项在您的命令中没有明确列出,但通常与 -n 和 -u(或 -t 对于 TCP)一起使用,以显示每个套接字/端口相关的进程。这可以帮助您确定哪个进程正在使用特定的端口。
因此,netstat -nup 命令将显示系统上所有 UDP 端口的监听状态,以及它们对应的本地和远程地址(以数字形式)。如果使用了 -p 选项,还会显示每个端口对应的进程。
请注意,为了使用 netstat 命令并查看所有进程信息,您可能需要具有足够的权限(通常是 root 用户或使用 sudo)。
另外,一些现代 Linux 发行版可能默认不安装 netstat 工具,而是推荐使用 ss 命令作为替代。ss 命令提供了类似的功能,但可能具有更好的性能和更多的选项。
显示的信息都是什么含义
netstat 命令用于显示网络连接、路由表、接口统计等网络相关信息。在你提供的输出中,它显示了 UDP 相关的连接信息。下面是对每个字段的解释:
Proto:
这个字段显示的是协议类型。在这里,我们看到 udp 和 udp6,分别表示 IPv4 的 UDP 协议和 IPv6 的 UDP 协议。
Recv-Q:
这个字段显示的是接收队列中等待读取的字节数。如果此值非零,可能表示有数据到达但还没有被进程读取。
Send-Q:
这个字段显示的是发送队列中等待发送的字节数。如果此值非零,可能表示进程尝试发送数据但还没有被发送出去。
Local Address:
这个字段显示的是本地地址和端口号。对于 0.0.0.0 和 ::,它们表示该服务正在监听所有可用的网络接口。对于 127.0.0.1 和 ::1,它们分别表示 IPv4 和 IPv6 的回环地址,也就是本地回环测试地址。
Foreign Address:
这个字段显示的是远程地址和端口号。对于监听套接字(也就是服务器套接字),远程地址通常是 *,表示可以接受来自任何远程地址的连接。
State:
这个字段显示的是套接字的状态。对于 UDP 套接字,通常不会显示像 TCP 那样的连接状态(如 LISTEN、ESTABLISHED 等),因为 UDP 是无连接的协议。这里的状态可能是空的,或者显示其他与 UDP 相关的状态信息。
PID/Program name:
这个字段显示的是与该套接字关联的进程 ID 和程序名。如果这里显示的是 -,可能表示 netstat 没有足够的权限来显示这些信息,或者该套接字没有与任何进程关联。
2.2客户端编程
客户端编程时要bind吗?
client一般不需要显示的bind指定port,而是让OS自动随机选择为什么?:如果显示的调用bind,那么此时的cilent一定bind了一个固定的ip+port,如果有其他客户端提前bind了这个port就出现不应该出现的情况了。
在客户端编程中,是否需要使用 bind 函数取决于具体的网络编程场景和所使用的协议。
在 TCP/IP 网络编程中,bind 函数通常用于将一个套接字绑定到一个特定的本地地址和端口上。然而,在客户端编程中,bind 的使用并不是必须的。当客户端创建套接字并准备连接到服务器时,操作系统通常会为客户端套接字自动分配一个本地端口号,而不需要显式调用 bind 函数。
在大多数情况下,客户端只需要调用 connect 函数来指定服务器的地址和端口,然后操作系统会负责建立与服务器的连接。在这种情况下,客户端不需要关心自己的本地地址和端口号,因为操作系统会自动处理这些细节。
然而,在某些特殊情况下,客户端可能需要显式地使用 bind 函数。例如,如果客户端需要在特定的本地端口上监听传入的连接(尽管这种情况更常见于服务器编程),或者如果客户端需要确保使用特定的本地地址进行连接(例如,在多网卡环境下选择特定的网络接口),那么可以使用 bind 函数来设置本地地址和端口。
总之,对于大多数客户端编程场景,通常不需要显式使用 bind 函数。但在某些特殊情况下,根据具体需求,可能需要调用 bind 函数来设置客户端套接字的本地地址和端口。
2.3运行测试
2.3.1本机通信
127.0.0.1:本地环回:client和server发送数据只在本地协议栈中进行数据流动,不会把数据发送到网络中。这种通常用于本地网络服务器的测试,如果本地测试通过,联网通常也不会错,如果还错可能是由于防火墙没关/端口没开放/网络不好。
- 云服务器无法直接绑定 公网IP或者所指定的非127.0.0.1或全零这样的IP,也就是一个具体的IP。
- client编好后,sz+可执行程序形成文件;把文件发给别的电脑;别的电脑rz后改权限chmod+x运行就可以实现通信。
2.3.2popen
在Linux(以及其他UNIX-like系统)中,popen函数是一个在C语言中使用的库函数,用于执行一个shell命令,并返回一个指向该命令的输入或输出的FILE指针。这允许你像操作普通文件一样来操作shell命令的输出或输入。
函数参数
popen函数接受两个参数:
const char *command:要执行的shell命令的字符串。
const char *type:一个指定打开模式的字符串,它决定了popen是读取命令的输出还是写入命令的输入。
“r”(或"read"):读取命令的输出。
“w”(或"write"):写入命令的输入。
返回值
popen函数返回一个FILE指针,指向一个流,这个流要么是命令的输出(如果type是"r"),要么是命令的输入(如果type是"w")。如果发生错误,popen返回NULL。
工作原理
popen函数的工作原理大致如下:
创建一个管道(pipe)。
调用fork来创建一个子进程。
在子进程中:
关闭管道的读端(如果type是"w")或写端(如果type是"r")。
将管道的另一端重定向到stdin(如果type是"w")或stdout(如果type是"r")。
执行提供的command。
在父进程中:
关闭管道的写端(如果type是"r")或读端(如果type是"w")。
返回指向管道的FILE指针。
函数功能
popen函数的主要功能是将shell命令的输出或输入作为C语言中的文件流来处理。这使得你可以使用标准的文件I/O函数(如fread、fwrite、fscanf、fprintf等)来读取命令的输出或写入命令的输入。
示例
下面是一个简单的示例,它使用popen来执行ls命令并读取其输出:
c
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *fp;
char path[1035];
/* 打开一个命令用于读取 */
fp = popen("ls -l", "r");
if (fp == NULL) {
printf("执行命令失败\n" );
exit(1);
}
/* 读取命令的输出 */
while (fgets(path, sizeof(path)-1, fp) != NULL) {
printf("%s", path);
}
/* 关闭 */
pclose(fp);
return 0;
}
注意:使用popen时要小心,因为它涉及到创建子进程和执行shell命令,这可能会带来安全隐患(例如命令注入攻击)。确保你传递给popen的命令是安全的,并且不依赖于来自不可信来源的输入。在可能的情况下,考虑使用更安全的方法来执行命令或获取数据。
当在Linux下进行网络编程时,服务端以特定的ip比如127.0.0.1和以0.0.0.0启动,有什么区别?
在Linux下进行网络编程时,服务端选择以特定的IP地址(如127.0.0.1)或以0.0.0.0启动,会有显著的区别。下面是两者之间的主要差异:
127.0.0.1(回环地址):
本地通信:当服务器绑定到127.0.0.1时,它只能接受来自本机(localhost)的连接请求。这是因为它是一个回环地址,用于在同一台机器上的应用程序之间进行通信。
安全性:使用127.0.0.1可以提高安全性,因为它限制了只有本地应用程序可以访问服务器。这有助于防止外部攻击者尝试连接或攻击服务器。
测试和开发:在开发和测试阶段,开发者经常将服务器绑定到127.0.0.1以确保只有本地客户端可以访问,从而简化测试和调试过程。
0.0.0.0(通配符地址):
监听所有可用接口:当服务器绑定到0.0.0.0时,它实际上是在监听机器上所有可用的网络接口。这意味着服务器可以接受来自任何IP地址的连接请求,无论是本地还是远程。
灵活性:使用0.0.0.0允许服务器在多个网络接口上运行,这对于具有多个IP地址或网络连接的机器特别有用。它提供了更大的灵活性,因为服务器可以响应来自不同网络的连接请求。
外部访问:当服务器需要接受来自外部网络(如互联网)的连接时,必须绑定到0.0.0.0或特定的公共IP地址。仅绑定到127.0.0.1将阻止外部访问。
总结:
选择127.0.0.1作为服务端绑定地址时,服务仅对本机开放,适用于本地测试和开发。
选择0.0.0.0作为服务端绑定地址时,服务将监听所有网络接口,允许来自本地和远程的连接请求,适用于生产环境或需要外部访问的场景。【把自己的udp_client文件发给朋友,朋友在他的服务器上运行,可以给你的响应式服务器发消息,云服务器要进行被远程访问,需要开放特定的端口。即server以8080启动,client也以8080启动,但是8080端口需要被开放二者才能通信】
在选择绑定地址时,请根据您的应用需求和网络环境进行决策。如果您只是想在本地进行开发和测试,那么使用127.0.0.1可能更合适。如果您希望服务器能够接受来自外部网络的连接请求,那么应该使用0.0.0.0或特定的公共IP地址。
2.3.3strcasestr
在Linux和其他UNIX-like系统中,strcasestr函数是一个C语言库函数,用于在一个字符串(haystack)中搜索另一个字符串(needle),同时忽略大小写。这个函数在GNU C库(glibc)中提供,但不是标准C库的一部分,因此可能不在所有的C库实现中都可用。
函数参数
strcasestr函数接受两个参数:
const char *haystack:这是要在其中进行搜索的原始字符串(也称为“大字符串”或“haystack”)。
const char *needle:这是要在大字符串中查找的子字符串(也称为“needle”)。
返回值
如果needle在haystack中找到,strcasestr返回一个指向haystack中首次出现needle的位置的指针。如果没有找到,函数返回NULL。
工作原理
strcasestr函数的工作原理大致如下:
它遍历haystack字符串中的每个字符。
对于haystack中的每个字符,它将其转换为小写(或大写,取决于实现),并与needle字符串的当前字符(同样转换为小写或大写)进行比较。
如果字符匹配,则继续比较haystack和needle的下一个字符。
如果在haystack中找到了与needle完全匹配的子字符串(忽略大小写),函数返回指向该子字符串在haystack中首次出现的位置的指针。
如果在遍历完整个haystack后仍未找到匹配项,函数返回NULL。
函数功能
strcasestr函数的主要功能是在一个字符串中搜索另一个字符串,同时忽略字符的大小写。这使得你可以在不区分大小写的情况下执行字符串搜索操作。这在处理用户输入或处理大小写不敏感的文本数据时非常有用。
示例
下面是一个简单的示例,演示了如何使用strcasestr函数:
c
#include <stdio.h>
#include <strings.h>
int main() {
const char *haystack = "Hello, World!";
const char *needle = "world";
char *result;
result = strcasestr(haystack, needle);
if (result != NULL) {
printf("Found '%s' in '%s' at position: %ld\n", needle, haystack, result - haystack);
} else {
printf("'%s' not found in '%s'\n", needle, haystack);
}
return 0;
}
在这个例子中,尽管haystack中的字符串是"Hello, World!“(注意"W"是大写的),strcasestr函数仍然能够找到"world”(小写),并输出它在haystack中的位置。
2.3.4回顾C++11智能指针
C++11中引入了智能指针的概念,以自动管理动态分配的内存,从而避免内存泄漏和其他相关问题。以下是auto_ptr、weak_ptr、unique_ptr和shared_ptr的简要介绍:
auto_ptr:
auto_ptr是C++98中引入的一个简单的智能指针,但在C++11中已经被废弃,因为其在某些情况下会导致意外的行为,特别是在所有权转移方面。
auto_ptr在析构时会自动删除它所指向的对象。
当一个auto_ptr被赋值给另一个时,所有权会转移,原来的auto_ptr会变为空。
unique_ptr:
unique_ptr是C++11中引入的,用于表示独占所有权的智能指针。
一个unique_ptr拥有其所指向的对象,当unique_ptr被销毁(例如超出作用域)时,它所指向的对象也会被自动删除。
unique_ptr不可复制,但可移动,这意味着你可以将一个unique_ptr的所有权转移给另一个unique_ptr。
shared_ptr:
shared_ptr是C++11中引入的,用于表示共享所有权的智能指针。
多个shared_ptr可以指向同一个对象,每个shared_ptr都有一个引用计数。当最后一个指向对象的shared_ptr被销毁时,对象才会被删除。
shared_ptr可以复制,这意味着你可以创建多个指向同一对象的shared_ptr。
weak_ptr:
weak_ptr是为了配合shared_ptr而引入的,它是对对象的一种弱引用,不会增加对象的引用计数。
weak_ptr主要是为了解决shared_ptr相互引用导致的循环引用问题。当一个shared_ptr和weak_ptr同时指向一个对象时,即使shared_ptr被销毁,只要weak_ptr还存在,对象就不会被删除。
weak_ptr可以观察一个对象,但并不会拥有该对象。当需要通过weak_ptr访问对象时,需要将其转换为shared_ptr,如果此时对象已经被删除,转换会失败。
总的来说,C++11中的智能指针提供了更加安全和方便的方式来管理动态分配的内存,减少了内存泄漏和其他内存相关问题的风险。在选择使用哪种智能指针时,应根据具体的使用场景和需求来决定。
3.linux网络涉及到的协议栈
Linux网络协议栈是一个复杂而强大的系统,它负责处理网络通信的各种细节。下面是对Linux网络协议栈的详细介绍:
套接字层(Socket Layer):
这是用户空间和内核空间之间的接口,提供了对底层网络通信的抽象。应用程序通过调用套接字API(如socket(), bind(), connect(), send(), recv()等)来与协议栈进行交互。
套接字层支持多种类型的套接字,如流式套接字(TCP)、数据报套接字(UDP)和原始套接字(直接访问网络层协议)。
传输层(Transport Layer):
传输层负责在源端和目的端之间建立可靠的或不可靠的数据传输。主要的传输层协议包括TCP(传输控制协议)和UDP(用户数据报协议)。
TCP提供面向连接的、可靠的、字节流的服务,通过序列号、确认和重传机制确保数据的完整性和顺序性。
UDP则提供无连接的、不可靠的数据报服务,不保证数据的顺序性和完整性,但具有较低的开销和较高的传输效率。
网络层(Network Layer):
网络层负责将数据包从源主机路由到目的主机。主要的网络层协议包括IP(互联网协议)和ICMP(互联网控制消息协议)。
IP协议负责在主机之间传输数据包,通过路由算法确定数据包的最佳路径。
ICMP协议用于在主机和路由器之间传递控制消息,如错误报告和路由查询。
数据链路层(Data Link Layer):
数据链路层负责在相邻节点之间传输数据帧。主要的协议包括以太网、PPP(点对点协议)等。
数据链路层还负责处理错误检测和流量控制等问题,确保数据帧在物理层上的可靠传输。
物理层(Physical Layer):
物理层负责将数据帧转换为电信号或光信号,以便在物理介质(如光纤、双绞线等)上进行传输。
物理层还负责处理信号的编码、解码和调制等问题,以确保数据的正确传输。
除了上述各层之外,Linux网络协议栈还包括一些辅助模块和子系统,如网络设备驱动程序、网络地址转换(NAT)、防火墙等,它们共同协作以实现复杂的网络通信功能。
在Linux内核中,网络协议栈的实现涉及大量的数据结构和算法,如套接字数据结构、路由表、缓冲区管理等。此外,Linux还提供了丰富的配置和调试工具,如ifconfig、netstat、tcpdump等,以帮助开发人员和运维人员更好地理解和控制网络行为。
总之,Linux网络协议栈是一个功能强大、灵活且可扩展的系统,它为用户提供了高效、可靠的网络通信服务。
协议栈和tcp/ip及osi模型有区别吗?
协议栈、TCP/IP模型以及OSI模型在网络通信中各自扮演着重要的角色,但它们之间确实存在一些关键的区别。
协议栈,又称协议堆叠,是计算机网络协议套件的一个具体的软件实现。每个协议模块通常都要和上下两个其他协议模块通信,可以想象成是协议栈中的层。协议栈反映了网络中文件传输的过程,并使得每个网络层能够处理特定的通信任务。
TCP/IP模型是一种网络通信协议模型,由传输控制协议(TCP)和互联网协议(IP)组成,是互联网通信的基础。它将网络通信分为四个层次:网络接口层、网络层、传输层和应用层。每一层都有其特定的功能和协议,共同协作以实现数据的可靠传输。
OSI(开放式系统互连)模型,即OSI参考模型,是ISO组织在1985年研究的网络互连模型。它定义了网络互连的七层框架:物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。OSI模型提供了一个对网络通信的全面描述,并为不同厂商和开发者提供了一个统一的开发标准。
从区别上来看:
TCP/IP模型与OSI模型的主要区别在于层数不同以及第三层的协议支持不同。TCP/IP模型仅支持IP协议,而OSI模型支持所有网络层协议。此外,TCP/IP协议栈支持跨层封装,这在某些情况下可以加快收敛速度和提高效率。
协议栈与TCP/IP模型和OSI模型的关系在于,协议栈是实现这些模型的具体软件形式。也就是说,TCP/IP模型和OSI模型为网络通信提供了理论框架和分层结构,而协议栈则是这些模型在实际网络环境中的软件实现。
总的来说,协议栈、TCP/IP模型和OSI模型各有其特点和功能,它们共同构成了现代网络通信的基础。在实际应用中,这些模型和协议栈相互协作,以实现高效、可靠的网络通信。
在Linux下进行网络编程时,先调用socket,然后bind,然后调用其他接口就能进行服务器和客户端通信了。原理是什么?socket,bind调用完之后发生了什么?为什么这样就能通信?
在Linux下进行网络编程时,确实需要按照特定的顺序调用函数来设置和建立服务器与客户端之间的通信。这个过程涉及到底层的网络协议和操作系统的支持。下面我将解释socket和bind调用后发生的事情,以及为什么它们能够支持网络通信。
socket调用
当你调用socket函数时,你实际上是在请求操作系统为你创建一个新的套接字(socket)。这个套接字是一个数据结构,它包含了进行网络通信所需的所有信息,比如协议类型(TCP、UDP等)、本地和远程的地址等。
创建套接字后,操作系统会为你分配一个唯一的文件描述符,用于后续对套接字的操作。这个文件描述符就像一个句柄,你可以通过它来读写套接字上的数据。
bind调用
bind函数用于将套接字与本地的一个地址(IP地址和端口号)关联起来。对于服务器来说,这一步是必须的,因为服务器需要监听一个特定的端口来接收客户端的连接请求。
当你调用bind时,操作系统会检查你指定的地址是否可用,并将其与套接字关联起来。这样,当客户端发送连接请求或数据时,操作系统就能知道应该将请求或数据发送到哪个套接字上。
其他接口调用
在socket和bind之后,服务器通常会调用listen来开始监听连接请求。当有客户端连接时,服务器会调用accept来接受连接,并返回一个新的套接字用于与客户端的通信。
对于客户端来说,它会在socket之后调用connect来尝试连接到服务器。一旦连接建立成功,客户端和服务器就可以通过各自的套接字来发送和接收数据了。
原理与通信能力
之所以能够通信,是因为操作系统和网络协议栈在背后默默地做了大量的工作。当你调用这些网络编程接口时,你实际上是在与操作系统的网络协议栈进行交互。
网络协议栈:它负责实现各种网络协议(如TCP、UDP、IP等),并将你的数据按照协议格式进行封装和解封装。这样,数据就能在网络中正确地传输和识别。
路由与转发:操作系统还负责根据目标地址选择合适的路径来发送数据。这涉及到路由表的查询和数据的转发。
错误处理与重传:对于可靠的协议(如TCP),网络协议栈还负责处理各种错误情况,比如数据丢失或乱序。它会自动进行数据的重传和排序,以确保数据的完整性和顺序性。
因此,当你调用socket和bind等函数时,你实际上是在利用操作系统和网络协议栈的功能来建立和管理网络通信。这些函数为你提供了高层次的接口,使得你可以更加方便地进行网络编程,而不需要关心底层的细节。
udp的全双工是如何实现的?
UDP(User Datagram Protocol,用户数据报协议)是一个无连接的、不可靠的传输层协议,它提供了全双工通信的能力。全双工通信意味着数据可以在同一时刻双向流动,即发送方和接收方都可以同时发送和接收数据。UDP实现全双工通信主要依赖于以下几个关键点:
无连接性:UDP不需要在通信双方之间建立连接。因此,发送方和接收方都可以随时开始发送数据,无需等待对方的确认或建立连接的过程。这种无连接特性使得UDP非常适合于需要快速响应和低延迟的应用场景。
独立的数据报:UDP将每个数据报视为独立的单元,每个数据报都包含源端口号、目的端口号、数据长度和校验和等信息。这意味着每个数据报都可以独立地路由和传输,无需与其他数据报保持顺序或关联。因此,发送方和接收方可以同时发送和接收多个数据报,实现全双工通信。
异步传输:UDP的异步传输特性意味着数据的发送和接收是异步进行的,即发送方发送数据后不需要等待接收方的确认或响应,可以继续发送其他数据。同样地,接收方也可以随时接收数据,无需等待发送方的进一步指示。这种异步性使得UDP能够支持并发通信和实时交互。
需要注意的是,虽然UDP提供了全双工通信的能力,但它并不保证数据的可靠传输。由于UDP是无连接的,它不提供流量控制、数据排序或重传机制。因此,在使用UDP进行通信时,应用程序需要自行处理这些问题,以确保数据的完整性和正确性。
总之,UDP通过其无连接性、独立的数据报和异步传输特性实现了全双工通信。这使得UDP成为许多需要快速响应和低延迟的应用场景的首选传输层协议。然而,在使用UDP时,应用程序需要仔细考虑如何处理数据的可靠性和完整性。
回顾静态成员函数
静态成员函数是C++中的一个重要特性,它们与普通的成员函数有一些显著的差异。静态成员函数属于类本身,而不是类的实例,这意味着它们可以在没有创建类的对象的情况下被调用。静态成员函数只能访问静态成员变量和其他静态成员函数,不能访问类的非静态成员。
静态成员函数的应用场景、目的和意义主要包括以下几个方面:
应用场景:
工具函数:当某些函数仅与类有关,但并不涉及类的具体实例时,可以将其设计为静态成员函数。例如,一个表示数学运算的类可能有一个静态成员函数来计算两个数的和,这个函数并不依赖于类的任何特定实例。
访问静态成员:静态成员函数常常用于访问和修改类的静态成员变量。由于静态成员变量不属于任何特定的对象实例,因此需要通过静态成员函数进行访问。
工厂方法:在设计模式中,静态成员函数经常用作工厂方法,用于创建类的实例。这种方法允许我们控制对象的创建过程,例如实现单例模式或实现对象的池化。
目的:
减少耦合:通过将某些功能设计为静态成员函数,可以减少类与其使用者之间的耦合,使得类的使用更加灵活和独立。
提供类级别的操作:静态成员函数允许我们在不创建类实例的情况下执行某些操作,这对于那些只需要在类级别执行的操作非常有用。
意义:
提高代码复用性:静态成员函数可以被类的所有实例共享,提高了代码的复用性。
简化代码设计:通过将某些功能设计为静态成员函数,可以简化类的设计,使得类的职责更加清晰和明确。
增强代码可读性:通过合理使用静态成员函数,可以使代码的结构更加清晰,提高代码的可读性。
总的来说,静态成员函数是C++中一种强大的工具,它们允许我们在类级别执行操作,提高代码的复用性和可读性,并简化代码设计。然而,我们也需要注意合理使用静态成员函数,避免过度使用导致代码结构混乱或增加不必要的复杂性。
4.三个版本的服务器
4.1响应式
#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP
#include "Log.hpp"
#include <iostream>
#include <unordered_map>
#include <cstdio>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <queue>
#define SIZE 1024
class UdpServer
{
public:
// 这里填空串 下面三目运算分配INADDR_ANY;填指定串 server以指定串作为ip
UdpServer(uint16_t port, std::string ip = "127.0.0.1")
: _ip(ip),
_port(port),
_socketFd(-1)
{
}
bool initServer()
{
// 新系统调用完成网络功能
// 1. socket创建套接字 协议用IPv4 套接字类型用数据报套接字
_socketFd = socket(AF_INET, SOCK_DGRAM, 0); // #define AF_INET PF_INET 二者相同
if (_socketFd < 0)
{
logMsg(FATAL, "socket::%d:%s", errno, strerror(errno));
exit(2); // 正规写法:规定每个退出码代表什么意思
}
// 2. bind绑定端口号 将一个套接字绑定到一个特定的ip和port
struct sockaddr_in svr_sockAddr;
bzero(&svr_sockAddr, sizeof(svr_sockAddr));
svr_sockAddr.sin_family = AF_INET;
// #define INADDR_ANY ((in_addr_t) 0x00000000) 字符串--32位整数--网络字节序
svr_sockAddr.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
svr_sockAddr.sin_port = htons(_port); //主机字节序--网络字节序
// int bind(int __fd, const sockaddr *__addr, socklen_t __len)
if (bind(_socketFd, (struct sockaddr *)&svr_sockAddr, sizeof(svr_sockAddr)) < 0)
{
logMsg(FATAL, "bind::%d:%s", errno, strerror(errno));
exit(2);
}
logMsg(NORMAL, "init udp server done ... %s", strerror(errno));
return true;
}
void Start()
{
// 响应式服务器:原封地不动返回client发送的消息
char buffer[SIZE];
while (true)
{
// clt_sockAddr 纯输出型参数
struct sockaddr_in clt_sockAddr;
bzero(&clt_sockAddr, sizeof(clt_sockAddr));
// clint_addrlen: 输入输出型参数
socklen_t clint_addrlen = sizeof(clt_sockAddr); // unsigned int
// 读取数据
ssize_t bytes_read = recvfrom(_socketFd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&clt_sockAddr, &clint_addrlen);
if (bytes_read == -1)
{
logMsg(FATAL, "recvfrom::%d:%s", errno, strerror(errno));
exit(3);
}
// 谁 发送的 什么信息
if (bytes_read > 0)
{
buffer[bytes_read] = 0; // 数据当做字符串使用
uint16_t client_port = ntohs(clt_sockAddr.sin_port); // 网络字节序 --> 主机字节序
std::string cli_ip = inet_ntoa(clt_sockAddr.sin_addr); // 4字节的网络序列的IP->本主机字符串风格的IP
printf("[%s:%d]# %s\n", cli_ip.c_str(), client_port, buffer);
}
// 回显数据
sendto(_socketFd, buffer, strlen(buffer), 0, (struct sockaddr *)&clt_sockAddr, clint_addrlen);
}
}
~UdpServer()
{
if (_socketFd >= 0)
close(_socketFd);
}
private:
// ip和port
std::string _ip;
uint16_t _port;
int _socketFd;
};
#endif
4.2命令式
#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP
#include "Log.hpp"
#include <iostream>
#include <unordered_map>
#include <cstdio>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <queue>
#define SIZE 1024
class UdpServer
{
public:
UdpServer(uint16_t port, std::string ip = "127.0.0.1")
: _ip(ip),
_port(port),
_socketFd(-1)
{
}
bool initServer()
{
// 新系统调用完成网络功能
// 1. socket创建套接字 协议用IPv4 套接字类型用数据报套接字
_socketFd = socket(AF_INET, SOCK_DGRAM, 0); // #define AF_INET PF_INET 二者相同
if (_socketFd < 0)
{
logMsg(FATAL, "socket::%d:%s", errno, strerror(errno));
exit(2); // 正规写法:规定每个退出码代表什么意思
}
// 2. bind绑定端口号 将一个套接字绑定到一个特定的地址和端口
struct sockaddr_in svr_sockAddr;
bzero(&svr_sockAddr, sizeof(svr_sockAddr));
svr_sockAddr.sin_family = AF_INET;
svr_sockAddr.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
svr_sockAddr.sin_port = htons(_port);
// int bind(int __fd, const sockaddr *__addr, socklen_t __len)
if (bind(_socketFd, (struct sockaddr *)&svr_sockAddr, sizeof(svr_sockAddr)) < 0)
{
logMsg(FATAL, "bind::%d:%s", errno, strerror(errno));
exit(2);
}
logMsg(NORMAL, "init udp server done ... %s", strerror(errno));
return true;
}
void Start()
{
char cmdBuf[SIZE];
while (true)
{
// client_addr 纯输出型参数
struct sockaddr_in client_addr;
bzero(&client_addr, sizeof(client_addr));
// clint_addrlen: 输入输出型参数
socklen_t clint_addrlen = sizeof(client_addr); // unsigned int
// 读取数据
ssize_t bytes_read = recvfrom(_socketFd, cmdBuf, sizeof(cmdBuf) - 1, 0, (struct sockaddr *)&client_addr, &clint_addrlen);
if (bytes_read == -1)
{
logMsg(FATAL, "recvfrom::%d:%s", errno, strerror(errno));
exit(3);
}
// 谁 发送的 什么信息
char partMsg[256];
std::string cmdOutput;
if (bytes_read > 0)
{
cmdBuf[bytes_read] = 0; // 数据当做字符串使用
// 不允许客户端执行rm命令
if (strcasestr(cmdBuf, "rm") != nullptr || strcasestr(cmdBuf, "rmdir") != nullptr)
{
std::string err_msg = "大坏蛋!不准删除!";
std::cout << "client send:" << cmdBuf << " but is blocked!" << std::endl;
sendto(_socketFd, err_msg.c_str(), err_msg.size(), 0, (struct sockaddr *)&client_addr, clint_addrlen);
continue;
}
FILE *fp = popen(cmdBuf, "r");
if (nullptr == fp)
{
logMsg(ERROR, "popen:%d:%s", errno, strerror(errno));
continue;
}
//popen把cmdBuf的命令执行的结果存入到fp文件中
while (fgets(partMsg, sizeof(partMsg), fp) != nullptr)
{
cmdOutput += partMsg;
}
fclose(fp);
sendto(_socketFd, cmdOutput.c_str(), cmdOutput.size(), 0, (struct sockaddr *)&client_addr, clint_addrlen);
}
}
}
~UdpServer()
{
if (_socketFd >= 0)
close(_socketFd);
}
private:
// ip和port
std::string _ip;
uint16_t _port;
int _socketFd;
};
#endif
4.3交互式
#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP
#include "Log.hpp"
#include <iostream>
#include <unordered_map>
#include <cstdio>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <queue>
#define SIZE 1024
class UdpServer
{
public:
UdpServer(uint16_t port, std::string ip = "")
: _ip(ip),
_port(port),
_socketFd(-1)
{
}
bool initServer()
{
// 新系统调用完成网络功能
// 1. socket创建套接字 协议用IPv4 套接字类型用数据报套接字
_socketFd = socket(AF_INET, SOCK_DGRAM, 0); // #define AF_INET PF_INET 二者相同
if (_socketFd < 0)
{
logMsg(FATAL, "%d:%s", errno, strerror(errno));
exit(2); // 正规写法:规定每个退出码代表什么意思
}
// 2. bind绑定端口号 将一个套接字绑定到一个特定的地址和端口
// 将用户设置的ip和port在内核中和我们当前的进程强关联
struct sockaddr_in svr_socketAddr;
bzero(&svr_socketAddr, sizeof(svr_socketAddr));
svr_socketAddr.sin_family = AF_INET;
svr_socketAddr.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
svr_socketAddr.sin_port = htons(_port);
// int bind(int __fd, const sockaddr *__addr, socklen_t __len)
if (bind(_socketFd, (struct sockaddr *)&svr_socketAddr, sizeof(svr_socketAddr)) < 0)
{
logMsg(FATAL, "%d:%s", errno, strerror(errno));
exit(2);
}
logMsg(NORMAL, "init udp server done ... %s", strerror(errno));
return true;
}
void Start()
{
char buffer[SIZE];
while (true)
{
// client_addr 纯输出型参数
struct sockaddr_in client_addr;
bzero(&client_addr, sizeof(client_addr));
// clint_addrlen: 输入输出型参数
socklen_t clint_addrlen = sizeof(client_addr); // unsigned int
// 读取数据
ssize_t bytes_read = recvfrom(_socketFd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client_addr, &clint_addrlen);
if (bytes_read == -1)
{
logMsg(FATAL, "%d:%s", errno, strerror(errno));
exit(3);
}
//一旦收到新用户的信息 就把新用户记录下来
char client[64];
if (bytes_read > 0)
{
buffer[bytes_read] = 0; // 数据当做字符串使用
std::string cli_ip = inet_ntoa(client_addr.sin_addr); // 4字节的网络序列的IP->本主机字符串风格的IP
uint16_t client_port = ntohs(client_addr.sin_port); // 网络字节序 --> 主机字节序
snprintf(client, sizeof(client), "%s-%u", cli_ip.c_str(), client_port); // 127.0.0.1-8080
logMsg(NORMAL, "client: %s", client);
std::string client_str = client;
auto it = _clients.find(client_str);
if (it == _clients.end())
{
logMsg(NORMAL, "add new client : %s", client);
_clients.insert({client_str, client_addr});
}
}
//svr把一个客户端发送过来的信息发送给了他所记录的所有的client
for (auto &iter : _clients)
{
std::string sendMsg = client;
sendMsg += "# ";
sendMsg += buffer; // 127.0.0.1-8080# 你好
logMsg(NORMAL, "push message to %s", iter.first.c_str());
sendto(_socketFd, sendMsg.c_str(), sendMsg.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second));
}
}
}
~UdpServer()
{
if (_socketFd >= 0)
close(_socketFd);
}
private:
// ip和port
std::string _ip;
uint16_t _port;
int _socketFd;
std::unordered_map<std::string, struct sockaddr_in> _clients;
/*for test: PC模型
多线程服务端: 解耦服务端 -- PC模型
A线程只读数据且把数据推送到队列
B线程只取数据且把数据发给客户端
std::queue<std::string> msgQueue;
如果消息是任务 还可以把任务交给线程池处理
*/
};
#endif
1.启动程序
当你执行以下命令序列时:
./server 0.0.0.0 8080
./client 127.0.0.1 8080
./client 127.0.0.1 8080
会发生以下事情:
服务器启动:
./server 0.0.0.0 8080 命令启动了一个服务器程序,该服务器绑定到所有可用的网络接口上的8080端口(由于使用了0.0.0.0地址)。服务器现在准备接收发送到8080端口的UDP数据包。
第一个客户端启动:
./client 127.0.0.1 8080 命令启动了第一个客户端程序。该客户端配置为向本地机器上的8080端口发送UDP数据包,目标地址是127.0.0.1(回环地址)。客户端发送的数据将被路由到同一台机器上运行的服务器。
第二个客户端启动:
几乎同时,./client 127.0.0.1 8080 命令又启动了第二个客户端程序。这个客户端与第一个客户端完全相同,它也向本地机器上的8080端口发送UDP数据包。
现在,服务器上有一个监听在8080端口的套接字,准备接收数据。两个客户端都在尝试向这个端口发送数据。由于UDP是无连接的,所以两个客户端可以独立地、同时地或几乎同时地向服务器发送数据。
服务器将分别接收来自两个客户端的数据包,并根据其程序逻辑处理这些数据。因为UDP是无状态的,所以服务器不会知道或关心数据是从一个客户端还是两个客户端发送的;它只处理到达其套接字的数据包。
需要注意的是,UDP不保证数据包的顺序、可靠性或重复检测,所以可能会有数据包丢失、乱序或重复到达的情况。此外,如果服务器没有设计为并发处理多个客户端的请求,它可能会在处理一个客户端的数据时阻塞另一个客户端的数据,这取决于服务器的实现细节。
在实际应用中,你通常需要确保服务器能够处理来自多个客户端的并发请求,以避免性能瓶颈或数据丢失。此外,如果数据包的内容需要按照特定的顺序处理,或者需要确保数据的可靠性,那么你可能需要考虑使用TCP而不是UDP。