文章目录
- 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智能指针
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中的智能指针提供了更加安全和方便的方式来管理动态分配的内存,减少了内存泄漏和其他内存相关问题的风险。在选择使用哪种智能指针时,应根据具体的使用场景和需求来决定。