当前位置: 首页 > news >正文

Linux网络编程-socket套接字使用详解

1.概念

        在Linux中,套接字(socket)是一种通信机制,用于实现不同进程之间或同一主机上的不同线程之间的数据交换。它是网络编程的基础,允许应用程序通过网络进行通信,也可以在同一台机器上的不同进程间进行通信。

        套接字的概念起源于BSD(Berkeley Software Distribution)操作系统,是由BSD UNIX提出并实现的。后来,套接字成为了Unix-like系统(包括Linux)中网络编程的标准接口。在早期的Unix系统中,进程间通信主要通过管道和命名管道(FIFO)实现,这些机制只适用于本地进程通信。为了能够在网络上进行进程间通信,套接字作为一种通用的解决方案被引入,并且得到了广泛的应用。

        套接字可以被视为一种文件描述符,它允许进程通过网络发送和接收数据。在Linux中,套接字可以基于网络协议(如TCP/IP、UDP)或本地通信协议(如UNIX域套接字)工作。它提供了一种统一的接口,使得应用程序可以通过不同的传输层协议来进行通信,而无需关心底层网络细节。

套接字类型

在Linux中,套接字可以根据其类型和地址族的不同而分为多种类型,主要包括:

  • 流套接字(Stream Socket):基于TCP协议,提供面向连接的可靠数据传输,数据传输顺序不会变化,适合需要可靠传输的应用。
  • 数据报套接字(Datagram Socket):基于UDP协议,提供不可靠的数据传输服务,传输速度快,但无法保证数据传输的顺序和可靠性,适合对传输效率要求较高的应用。
  • 原始套接字(Raw Socket):允许应用程序直接访问网络协议,如IP层,用于实现自定义网络协议或进行网络数据包分析等特殊用途。
  • UNIX域套接字(Unix Domain Socket):用于在同一台主机上的进程间通信,不涉及网络通信,提供了一种高效的本地通信机制。

2.字节序

        字节序(Byte Order)是指多字节数据在存储器中的存放顺序。由于计算机内存和存储器是以字节为最小单位进行寻址的,多字节数据(比如16位、32位、64位整数)在存储器中占据连续的字节空间。字节序定义了这些字节在存储器中的排列顺序。对于单字符来说是没有字节序问题的,字符串是单字符的集合,因此字符串也没有字节序问题。

大端字节序(Big Endian)

在大端字节序中,数据的高字节(Most Significant Byte,MSB)存储在低地址,低字节(Least Significant Byte,LSB)存储在高地址。这种方式类似于把一个多字节整数的数字本身按照从高位到低位的顺序存放在内存中。

小端字节序(Little Endian)

在小端字节序中,数据的低字节(LSB)存储在低地址,高字节(MSB)存储在高地址。这种方式将一个多字节整数的最低有效字节放在最低地址处。

// 有一个16进制的数, 有32位 (int): 0xab5c01ff
// 字节序, 最小的单位: char 字节, int 有4个字节, 需要将其拆分为4份
// 一个字节 unsigned char, 最大值是 255(十进制) ==> ff(16进制) 内存低地址位                内存的高地址位
--------------------------------------------------------------------------->
小端:         0xff        0x01        0x5c        0xab
大端:         0xab        0x5c        0x01        0xff
  • 网络通信

  • 大多数网络协议(如TCP/IP、HTTP)规定数据传输时采用网络字节序,即大端字节序。这是因为网络协议需要确保通信双方能够统一数据的解析方式,避免因字节序问题导致数据解析错误。
  • 在网络中,通常使用的是网络字节序(大端字节序),因此,如果要与网络进行数据交换,尤其是对于传输整数等多字节数据时,使用大端字节序能够简化数据的处理和解析。
  • 个人计算机

    • 大多数个人计算机(如x86架构)采用小端字节序。因此,在开发和编写面向这些平台的应用程序时,通常会使用小端字节序。
    • Windows、Linux(x86、x86-64架构)、以及大部分现代桌面和移动设备的处理器都是小端字节序。
  • 内存访问优化

    • 小端字节序在访问多字节数据时有时可以更加高效。例如,访问一个32位整数的低位字节时可以直接通过该整数的地址加1来获取,而不需要进行字节顺序的转换。

相关函数:

#include <arpa/inet.h>
功能:将32位主机字节序整数转换为网络字节序(大端字节序)。
uint32_t htonl(uint32_t hostlong);
参数:hostlong:待转换的32位主机字节序整数。
返回值:返回转换后的32位网络字节序整数。
注意事项:
用于将主机字节序数据转换为网络字节序,以便进行网络通信。
如果主机字节序和网络字节序相同(通常是小端字节序的情况下),则 htonl 函数不会进行实际的字节序转换,直接返回输入参数本身。
在网络编程中,发送数据前通常要使用此函数将数据转换为网络字节序。功能:将16位主机字节序短整数转换为网络字节序(大端字节序)。
uint16_t htons(uint16_t hostshort);
参数:hostshort:待转换的16位主机字节序短整数。
返回值:返回转换后的16位网络字节序短整数。
注意事项:
用于将主机字节序数据转换为网络字节序,以便进行网络通信。
在网络编程中,发送数据前通常要使用此函数将数据转换为网络字节序。功能:将32位网络字节序(大端字节序)整数转换为主机字节序。
uint32_t ntohl(uint32_t netlong);
参数:netlong:待转换的32位网络字节序整数。
返回值:返回转换后的32位主机字节序整数。
注意事项:
用于将接收到的网络字节序数据转换为主机字节序,以便应用程序正确解析和处理数据。
在接收网络数据后,通常要使用此函数将数据转换为主机字节序进行处理。功能:将16位网络字节序(大端字节序)短整数转换为主机字节序。
uint16_t ntohs(uint16_t netshort);
参数:netshort:待转换的16位网络字节序短整数。
返回值:返回转换后的16位主机字节序短整数。
注意事项:
用于将接收到的网络字节序数据转换为主机字节序,以便应用程序正确解析和处理数据。
在接收网络数据后,通常要使用此函数将数据转换为主机字节序进行处理。

示例代码:

#include <stdio.h>
#include <arpa/inet.h> // 包含网络字节序转换函数的头文件int main() {// 定义一个主机字节序的32位整数uint32_t host_long = 0x12345678;// 定义一个主机字节序的16位短整数uint16_t host_short = 0x1234;// 将主机字节序的整数转换为网络字节序(大端字节序)uint32_t network_long = htonl(host_long);// 将主机字节序的短整数转换为网络字节序(大端字节序)uint16_t network_short = htons(host_short);// 输出转换前后的整数值和短整数值printf("Original Host Long: 0x%x\n", host_long);printf("Network Long (Big Endian): 0x%x\n", network_long);printf("Original Host Short: 0x%x\n", host_short);printf("Network Short (Big Endian): 0x%x\n", network_short);// 将网络字节序的整数转换回主机字节序uint32_t host_long_back = ntohl(network_long);// 将网络字节序的短整数转换回主机字节序uint16_t host_short_back = ntohs(network_short);// 输出转换回主机字节序后的整数值和短整数值printf("\nNetwork Long (Big Endian): 0x%x\n", network_long);printf("Back to Host Long: 0x%x\n", host_long_back);printf("Network Short (Big Endian): 0x%x\n", network_short);printf("Back to Host Short: 0x%x\n", host_short_back);return 0;
}

3.IP地址转换

虽然IP地址本质是一个整形数,但是在使用的过程中都是通过一个字符串来描述,下面的函数描述了如何将一个字符串类型的IP地址进行大小端转换:

3.1 inet_pton 函数

#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
功能:将点分十进制字符串形式的IP地址转换为网络字节序的二进制IP地址表示。
参数:
af:地址族(Address Family),可以是 AF_INET 表示IPv4,或 AF_INET6 表示IPv6。
src:待转换的点分十进制字符串形式的IP地址。
dst:指向存放转换后二进制IP地址的内存空间的指针。
返回值:
如果转换成功,返回1(IPv4)或者1(IPv6)。
如果传入的字符串不是合法的IP地址,返回0。
如果发生错误,返回-1,并设置 errno 指示具体错误。
注意事项:
dst 参数应该足够大来容纳转换后的二进制IP地址。
在使用前需要确保正确设置 af 参数,以指明是处理IPv4还是IPv6地址。
函数会自动识别并转换点分十进制的IPv4地址和IPv6的十六进制地址。

3.2 inet_ntop 函数

#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
功能:将网络字节序的二进制IP地址表示转换为点分十进制字符串形式的IP地址。
参数:
af:地址族(Address Family),可以是 AF_INET 表示IPv4,或 AF_INET6 表示IPv6。
src:指向存放二进制IP地址的内存空间的指针。
dst:用于存放转换后的点分十进制字符串形式IP地址的缓冲区。
size:缓冲区 dst 的大小,一般建议使用 INET_ADDRSTRLEN(IPv4地址的最大长度)或 INET6_ADDRSTRLEN(IPv6地址的最大长度)。
返回值:
如果转换成功,返回指向 dst 的指针,即转换后的点分十进制字符串形式IP地址。
如果发生错误,返回 NULL,并设置 errno 指示具体错误。
注意事项:
dst 缓冲区应足够大以容纳转换后的IP地址字符串。
函数根据 af 参数的值自动识别并转换二进制IP地址表示。
在使用前要确保 src 指向的内存区域大小足够。

示例代码:

#include <stdio.h>
#include <arpa/inet.h>
#include <errno.h>int main() {char ip4_str[] = "192.168.1.1";char ip6_str[] = "2001:0db8:85a3:0000:0000:8a2e:0370:7334";struct in_addr ip4_addr;struct in6_addr ip6_addr;char ip_str[INET6_ADDRSTRLEN];// 将IPv4字符串转换为二进制格式if (inet_pton(AF_INET, ip4_str, &ip4_addr) <= 0) {perror("inet_pton");return 1;}// 将二进制IPv4地址转换为字符串格式const char *ip4_str_converted = inet_ntop(AF_INET, &ip4_addr, ip_str, INET_ADDRSTRLEN);if (ip4_str_converted == NULL) {perror("inet_ntop");return 1;}printf("IPv4地址: %s\n", ip4_str_converted);// 将IPv6字符串转换为二进制格式if (inet_pton(AF_INET6, ip6_str, &ip6_addr) <= 0) {perror("inet_pton");return 1;}// 将二进制IPv6地址转换为字符串格式const char *ip6_str_converted = inet_ntop(AF_INET6, &ip6_addr, ip_str, INET6_ADDRSTRLEN);if (ip6_str_converted == NULL) {perror("inet_ntop");return 1;}printf("IPv6地址: %s\n", ip6_str_converted);return 0;
}

4.socket套接字

4.1相关操作函数

4.1.1 socket 函数

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:创建一个新的套接字。
参数:
domain:指定协议族,常见的有 AF_INET(IPv4)和 AF_INET6(IPv6),还有其他如 AF_UNIX(Unix域),AF_LOCAL(本地通信)等。
type:指定套接字类型,如 SOCK_STREAM(流式套接字,用于TCP),SOCK_DGRAM(数据报套接字,用于UDP),SOCK_RAW(原始套接字)等。
protocol:指定具体的协议,通常设为0以选择默认协议。
返回值:
如果成功,返回一个非负的套接字描述符,用于后续的套接字操作。
如果失败,返回 -1,并设置 errno 指示具体错误。
注意事项:
创建套接字时,需要确保传入正确的 domain、type 和 protocol 参数。
套接字描述符是一个整数,用于唯一标识一个套接字,应该小心管理,防止资源泄露。
在使用完套接字后,应该通过 close 函数关闭套接字,释放相关资源。

4.1.2 bind 函数

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:将一个本地地址(IP地址和端口号)分配给一个套接字。
参数:
sockfd:套接字描述符,由 socket 函数返回。
addr:指向包含要绑定到套接字的地址信息的结构体指针,通常是 struct sockaddr 结构体。
addrlen:addr 结构体的长度。
返回值:
如果成功,返回 0。
如果失败,返回 -1,并设置 errno 指示具体错误。
注意事项:
在使用 bind 函数前,确保套接字已经创建成功,并且填充了正确的地址信息到 addr 结构体中。
只有未被占用的地址才能成功绑定,否则会返回错误。
需要特别注意端口号的使用,避免与系统中已有的服务冲突。

4.1.3 struct sockaddr结构体

struct sockaddr 是用于存储各种套接字地址的通用结构体,在网络编程中广泛使用。它的设计灵活,可以适应不同协议族(如IPv4、IPv6、Unix域等)的地址表示。在写数据的时候不好用。struct sockaddr 的定义通常在 <sys/socket.h> 头文件中,是一个通用的套接字地址结构体。

struct sockaddr {sa_family_t sa_family;      // 地址族(Address Family)char        sa_data[14];    // 地址数据(包括IP地址和端口号)端口(2字节) + IP地址(4字节) + 填充(8字节)
};
sa_family:用于指定地址的协议族(Address Family),可以是以下常见的值之一:AF_INET:IPv4地址族AF_INET6:IPv6地址族AF_UNIX 或 AF_LOCAL:Unix域(本地通信)其他协议族的值,如AF_PACKET等,根据具体需要定义。
sa_data:存放套接字地址的实际数据部分,包括IP地址和端口号等。由于不同协议的地址数据可能不同,这里使用了一个固定长度的数组来存储。

struct sockaddr_in 是用于表示IPv4套接字地址的结构体,在网络编程中经常使用。它是 struct sockaddr 结构体的一个特定实现,用于IPv4地址族。struct sockaddr_in 的定义通常在 <netinet/in.h> 头文件中,用于表示IPv4地址的套接字地址结构体。

struct in_addr
{in_addr_t s_addr;
};  struct sockaddr_in {sa_family_t    sin_family; // 地址族 (AF_INET)in_port_t      sin_port;   // 端口号 (使用网络字节序)struct in_addr sin_addr;   // IPv4地址char           sin_zero[8]; // 填充字节,用于使结构体与 struct sockaddr 兼容
};
sin_family:地址族,固定为 AF_INET,表示IPv4地址族。
sin_port:16位端口号,使用网络字节序(即大端字节序)表示。
sin_addr:struct in_addr 类型的结构体,用于存储IPv4地址。
sin_zero:填充字段,使 struct sockaddr_in 的大小与 struct sockaddr 相同,用于兼容性。

4.1.4 listen 函数

#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能:将未连接的套接字转换为被动监听状态,用于接受客户端的连接请求。
参数:
sockfd:套接字描述符,由 socket 函数返回,并且已经通过 bind 绑定了本地地址。
backlog:指定同时等待处理的连接请求的最大数量,最大值为128
返回值:
如果成功,返回 0。
如果失败,返回 -1,并设置 errno 指示具体错误。
注意事项:
在调用 listen 函数前,套接字必须已经成功绑定到一个本地地址。
backlog 参数指定内核中连接队列的长度,影响服务器可以接受的最大连接数。
当有新的连接请求到达时,服务器将从队列中取出并处理。

4.1.5 accept 函数

#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:接受客户端的连接请求,创建一个新的套接字用于与客户端通信。
参数:
sockfd:套接字描述符,处于监听状态的套接字。
addr:(可选)指向用于存放客户端地址信息的结构体指针,通常是 struct sockaddr 结构体。
addrlen:(可选)addr 结构体的长度指针。
返回值:
如果成功,返回一个新的套接字描述符,用于与客户端通信。
如果失败,返回 -1,并设置 errno 指示具体错误。
注意事项:
accept 函数通常在服务器的主循环中调用,用于接受新的客户端连接。
如果不需要获取客户端的地址信息,可以将 addr 和 addrlen 参数设置为 NULL。
新创建的套接字用于与特定的客户端进行通信,应在通信结束后及时关闭。

4.1.6 接收和发送数据函数

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:将数据发送到连接的套接字。
参数:
sockfd:套接字描述符,指定要发送数据的套接字。
buf:指向要发送数据的缓冲区的指针。
len:要发送数据的字节数。
flags:指定发送操作的标志,通常设为 0。
返回值:
如果成功,返回发送的字节数。
如果失败,返回 -1,并设置 errno 指示具体错误。
注意事项:
send 函数可能会发送比请求的数据少的字节数(部分发送),应该在循环中调用直到所有数据都被发送。
需要注意处理信号中断(EINTR)的情况,以确保数据完整性和稳定性。ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能:从连接的套接字接收数据。
参数:
sockfd:套接字描述符,指定要接收数据的套接字。
buf:指向接收数据的缓冲区的指针。
len:要接收数据的最大字节数。
flags:指定接收操作的标志,通常设为 0。
返回值:
如果成功,返回接收的字节数。
如果连接关闭(对于 TCP 套接字),返回 0。
如果失败,返回 -1,并设置 errno 指示具体错误。
注意事项:
recv 函数可能会接收比请求的数据少的字节数(部分接收),应该在循环中调用直到接收到所需的数据或者达到预期的条件。
对于非阻塞套接字,需要处理 EAGAIN 或 EWOULDBLOCK 错误。
在使用前确保套接字已经连接或者绑定,并且合适地设置了 buf 和 len。
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
功能:向文件描述符 fd 写入数据。
参数:
fd:文件描述符,可以是套接字描述符。
buf:指向要写入数据的缓冲区的指针。
count:要写入的字节数。
返回值:
如果成功,返回实际写入的字节数。
如果失败,返回 -1,并设置 errno 指示具体错误。
注意事项:
write 函数通常用于向已连接的套接字写入数据,也可以用于向文件、管道等写入数据。
如果 write 返回值小于 count,则可能是由于部分写入或者错误发生。
应该在循环中调用 write 直到所有数据都被写入,或者处理写入失败的情况。ssize_t read(int fd, void *buf, size_t count);
功能:从文件描述符 fd 读取数据。
参数:
fd:文件描述符,可以是套接字描述符。
buf:指向存放读取数据的缓冲区的指针。
count:要读取的最大字节数。
返回值:
如果成功,返回实际读取的字节数。
如果已经到达文件末尾(对套接字来说通常表示连接关闭),返回 0。
如果失败,返回 -1,并设置 errno 指示具体错误。
注意事项:
read 函数通常用于从已连接的套接字读取数据,也可以用于从文件、管道等读取数据。
应该在循环中调用 read 直到接收到所需的数据,或者处理读取失败的情况。
对于非阻塞套接字,需要处理 EAGAIN 或 EWOULDBLOCK 错误。

在使用 socket 套接字进行网络通信时,特别是在 UDP 协议中,常用的数据发送和接收函数包括 sendtorecvfrom。这两个函数与 sendrecv 在功能上类似,但是更适用于无连接的 UDP 套接字,也可以用于有连接的套接字。

#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
功能:向指定地址发送数据。
参数:
sockfd:套接字描述符。
buf:指向要发送数据的缓冲区的指针。
len:要发送的数据字节数。
flags:发送标志,通常设置为 0。
dest_addr:指向目标地址结构体的指针,包含目标地址和端口信息。
addrlen:dest_addr 结构体的长度。
返回值:
如果成功,返回实际发送的字节数。
如果失败,返回 -1,并设置 errno 指示具体错误。
注意事项:
sendto 适用于无连接的 UDP 套接字,也可以用于有连接的套接字。
如果 dest_addr 是 NULL,则需要在之前使用 connect 函数连接套接字。
可以用于向多个目标发送数据,通过不同的 dest_addr 参数指定。ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);功能:从指定地址接收数据。
参数:
sockfd:套接字描述符。
buf:指向存放接收数据的缓冲区的指针。
len:缓冲区的大小,即最多接收的数据字节数。
flags:接收标志,通常设置为 0。
src_addr:指向发送方地址结构体的指针,用于存放发送方的地址信息。
addrlen:src_addr 结构体的长度指针,调用前需设置为结构体的实际长度。
返回值:
如果成功,返回实际接收的字节数。
如果没有可用数据且对方关闭连接,返回 0。
如果失败,返回 -1,并设置 errno 指示具体错误。
注意事项:
recvfrom 适用于无连接的 UDP 套接字,也可以用于有连接的套接字。
如果套接字已经连接(通过 connect 函数),则可以将 src_addr 和 addrlen 设置为 NULL。
可以用于从多个发送方接收数据,通过 src_addr 参数获取发送方的地址信息。

4.1.7 connect 函数

#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:与指定地址的服务器建立连接。
参数:
sockfd:套接字描述符,即 socket 函数返回的套接字描述符。
addr:指向 struct sockaddr 结构体的指针,包含要连接的服务器地址信息。
addrlen:addr 结构体的长度。
返回值:
如果成功,返回 0。
如果失败,返回 -1,并设置 errno 指示具体错误。
注意事项:
在调用 connect 前,需要先创建好套接字并填充好服务器的地址信息。
对于阻塞套接字,connect 函数可能会阻塞直到连接建立或超时。
对于非阻塞套接字,可能返回 EINPROGRESS,需要进一步检查连接状态。

4.2 TCP通信流程

TCP是一个面向连接的,安全的,流式传输协议,这个协议是一个传输层协议。

  • 连接导向

    • TCP 是面向连接的协议,通信双方在传输数据前需要先建立连接,确保数据可靠传输。
    • 连接的建立包括三次握手过程,保证了通信双方的可靠性和数据同步性。
  • 可靠性

    • TCP 提供可靠的数据传输,通过序号、确认应答、重传机制等手段来确保数据的完整性和可靠性。
    • 数据传输过程中,如果发生丢包、出错或者顺序错乱,TCP 会进行重传,直到数据正确送达目标。
  • 流量控制

    • TCP 使用滑动窗口协议进行流量控制,通过动态调整发送方的发送窗口大小,控制发送数据的速率,避免数据包丢失和网络拥塞。
  • 有序性

    • TCP 保证数据传输的有序性,发送的数据包按照顺序到达接收端,并且按照发送的顺序重组。

  • 服务端和客户端初始化 socket,得到文件描述符;
  • 服务端调用 bind,将 socket 绑定在指定的 IP 地址和端口;
  • 服务端调用 listen,进行监听;
  • 服务端调用 accept,等待客户端连接;
  • 客户端调用 connect,向服务端的地址和端口发起连接请求;
  • 服务端 accept 返回用于传输的 socket 的文件描述符;
  • 客户端调用 write 写入数据;服务端调用 read 读取数据;
  • 客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。

这里需要注意的是,服务端调用 accept 时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。

所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket

成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。

4.2.1 示例代码

TCP回显服务器

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <ctype.h> // 包含toupper函数#define PORT 8080
#define MAX_CLIENTS 5
#define BUFFER_SIZE 1024int main() {int server_fd, new_socket, valread;struct sockaddr_in address;int opt = 1;int addrlen = sizeof(address);char buffer[BUFFER_SIZE] = {0};char *hello = "Hello from server";// 创建 TCP 套接字if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}// 设置套接字选项,允许地址重用if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {perror("setsockopt");exit(EXIT_FAILURE);}// 设置服务器地址结构address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY; // 使用本地IP地址address.sin_port = htons(PORT);// 将套接字绑定到指定地址和端口if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");exit(EXIT_FAILURE);}// 监听连接请求,最多支持 MAX_CLIENTS 个客户端连接if (listen(server_fd, MAX_CLIENTS) < 0) {perror("listen");exit(EXIT_FAILURE);}printf("Server listening on port %d...\n", PORT);// 接受连接并与客户端通信while (1) {// 等待新连接if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {perror("accept");exit(EXIT_FAILURE);}// 打印客户端地址信息char client_addr[INET_ADDRSTRLEN];inet_ntop(AF_INET, &address.sin_addr, client_addr, INET_ADDRSTRLEN);printf("New connection from %s:%d\n", client_addr, ntohs(address.sin_port));valread = read(new_socket, buffer, BUFFER_SIZE);if (valread <= 0) {break;}// 将接收到的消息转换为大写for (int i = 0; i < valread; ++i) {buffer[i] = toupper(buffer[i]);}printf("Received message from %s:%d: %s\n", client_addr, ntohs(address.sin_port), buffer);// 发送转换后的消息给客户端send(new_socket, buffer, valread, 0);memset(buffer, 0, sizeof(buffer));// 关闭与客户端的连接printf("Client disconnected: %s:%d\n", client_addr, ntohs(address.sin_port));}close(new_socket);close(server_fd);return 0;
}
//客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>#define PORT 8080
#define BUFFER_SIZE 1024
#define SERVER_IP "127.0.0.1"int main() {int sock = 0, valread;struct sockaddr_in serv_addr;char buffer[BUFFER_SIZE] = {0};char input_buffer[BUFFER_SIZE] = {0};char *hello = "Hello from client";// 创建 TCP 套接字if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {perror("socket creation failed");return -1;}// 设置服务器地址结构serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(PORT);// 将 IPv4 地址从文本转换为二进制形式if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {perror("Invalid address/ Address not supported");return -1;}// 连接服务器if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {perror("Connection Failed");return -1;}printf("Connected to server\n");// 循环发送消息并接收响应while (1) {printf("Enter message to send (or 'exit' to quit): ");fgets(input_buffer, BUFFER_SIZE, stdin);// 去掉输入的换行符input_buffer[strcspn(input_buffer, "\n")] = 0;// 如果输入是 'exit',则退出循环if (strcmp(input_buffer, "exit") == 0) {break;}// 发送消息给服务器send(sock, input_buffer, strlen(input_buffer), 0);printf("Message sent to server: %s\n", input_buffer);// 接收服务器的响应valread = read(sock, buffer, BUFFER_SIZE);printf("Server response: %s\n", buffer);memset(buffer, 0, sizeof(buffer));}close(sock);return 0;
}

4.3 UDP通信流程

UDP是一个简单的、无连接的、使用数据报的,轻量级的传输层协议。

  • 无连接

    • UDP 是无连接的协议,通信双方在传输数据时不需要建立连接,可以直接发送数据包。
    • 没有连接建立过程,因此UDP的开销比TCP小,适合对实时性要求较高的应用。
  • 不可靠性

    • UDP 不提供数据传输的可靠性保证,发送数据后不会确认是否到达目标,也不会进行重传。
    • 发送的数据包可能丢失或者无序到达接收端,需要应用层自行处理数据的丢失和重传。
  • 速度和效率

    • UDP 相比TCP速度更快,没有建立连接和维护状态的开销,适合实时性要求高、传输数据量小的应用。
    • UDP 的头部开销小,每个数据包仅包含基本的必要信息,传输效率较高。
  • 广播和多播

    • UDP 支持广播和多播,可以将数据包发送到一个网络中的多个接收端。

UDP通信流程概述

  1. UDP发送方初始化套接字,得到文件描述符
  2. UDP接收方初始化套接字,得到文件描述符
  3. UDP接收方调用bind,将套接字绑定在指定的IP地址和端口
  4. UDP发送方调用sendto发送数据到接收方的地址和端口
  5. UDP接收方调用recvfrom接收数据
  6. UDP接收方处理请求并调用sendto发送响应数据到发送方
  7. UDP发送方调用recvfrom接收响应数据
  8. 通信结束后,发送方和接收方分别调用close关闭套接字

4.3.1 示例代码

服务器端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <ctype.h> // 包含toupper函数#define PORT 8080
#define BUFFER_SIZE 1024int main() {int sockfd;struct sockaddr_in server_addr, client_addr;char buffer[BUFFER_SIZE];socklen_t addr_len;int n;// 创建UDP套接字if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {perror("socket creation failed");exit(EXIT_FAILURE);}// 设置服务器地址结构memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(PORT);// 绑定套接字到指定IP地址和端口if (bind(sockfd, (const struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {perror("bind failed");close(sockfd);exit(EXIT_FAILURE);}printf("Server listening on port %d...\n", PORT);while (1) {// 接收客户端的数据addr_len = sizeof(client_addr);n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, (struct sockaddr *)&client_addr, &addr_len);buffer[n] = '\0';// 打印客户端地址信息和接收到的数据char client_ip[INET_ADDRSTRLEN];inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);printf("Received message from %s:%d: %s\n", client_ip, ntohs(client_addr.sin_port), buffer);// 将数据转换为大写for (int i = 0; i < n; i++) {buffer[i] = toupper(buffer[i]);}// 发送转换后的数据回客户端sendto(sockfd, buffer, n, 0, (struct sockaddr *)&client_addr, addr_len);printf("Sent uppercase message to %s:%d: %s\n", client_ip, ntohs(client_addr.sin_port), buffer);}// 关闭套接字close(sockfd);return 0;
}

客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>#define PORT 8080
#define BUFFER_SIZE 1024
#define SERVER_IP "127.0.0.1"int main() {int sockfd;struct sockaddr_in server_addr;char buffer[BUFFER_SIZE];char recv_buffer[BUFFER_SIZE];socklen_t addr_len;int n;// 创建UDP套接字if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {perror("socket creation failed");exit(EXIT_FAILURE);}// 设置服务器地址结构memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(PORT);inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);printf("Connected to server at %s:%d\n", SERVER_IP, PORT);while (1) {printf("Enter message to send (or 'exit' to quit): ");fgets(buffer, BUFFER_SIZE, stdin);buffer[strcspn(buffer, "\n")] = 0; // 去掉输入的换行符// 如果输入是 'exit',则退出循环if (strcmp(buffer, "exit") == 0) {break;}// 发送数据到服务器sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&server_addr, sizeof(server_addr));printf("Message sent to server: %s\n", buffer);// 接收服务器的响应addr_len = sizeof(server_addr);n = recvfrom(sockfd, recv_buffer, BUFFER_SIZE, 0, (struct sockaddr *)&server_addr, &addr_len);recv_buffer[n] = '\0';printf("Server response: %s\n", recv_buffer);}// 关闭套接字close(sockfd);return 0;
}

相关文章:

Linux网络编程-socket套接字使用详解

1.概念 在Linux中&#xff0c;套接字&#xff08;socket&#xff09;是一种通信机制&#xff0c;用于实现不同进程之间或同一主机上的不同线程之间的数据交换。它是网络编程的基础&#xff0c;允许应用程序通过网络进行通信&#xff0c;也可以在同一台机器上的不同进程间进行通…...

Leetcode 236. 二叉树的最近公共祖先

142. 环形链表 II 问题描述 给定一个链表的头节点 head &#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返回 null。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&am…...

GPT-4从0到1搭建一个Agent简介

GPT-4从0到1搭建一个Agent简介 1. 引言 在人工智能领域&#xff0c;Agent是一种能够感知环境并采取行动以实现特定目标的系统。本文将简单介绍如何基于GPT-4搭建一个Agent。 2. Agent的基本原理 Agent的核心是感知-行动循环&#xff08;Perception-Action Loop&#xff09;…...

docker镜像源配置

docker默认的镜像源&#xff0c;走的是国外网络&#xff0c;下载速度感人&#xff0c;修改镜像源&#xff0c;进入/etc/docker/ cd /etc/docker 编辑文件daemon.json(没有就直接创建)&#xff0c;内容&#xff1a; {"registry-mirrors": ["https://q7ta64ip.…...

解读InnoDB数据库索引页与数据行的紧密关联

目录 一、快速走进索引页结构 &#xff08;一&#xff09;整体展示说明 &#xff08;二&#xff09;内容说明 File Header&#xff08;文件头部&#xff09; Page Header&#xff08;页面头部&#xff09; Infimum Supremum&#xff08;最小记录和最大记录&#xff09; …...

以数据编织,重构数据管理新范式

大数据产业创新服务媒体 ——聚焦数据 改变商业 人工智能几乎统一了全球最顶尖科技公司的认知&#xff1a;这个时代&#xff0c;除了AI&#xff0c;没有第二条路可走。 人工智能的技术逻辑颇有一种“暴力美学”&#xff0c;它依托于海量大数据和超高算力的训练和推理&#xff…...

在linux x86服务器安装jdk

安装JDK&#xff08;Java Development Kit&#xff09;在Linux x86 服务器上可以按照以下步骤进行操作。以下步骤假设你有root权限或者sudo权限。 1. 下载JDK安装包 首先&#xff0c;你需要从Oracle官网或者OpenJDK官网下载JDK的安装包。可以选择对应的版本&#xff0c;比如J…...

2024智慧竞技游戏俱乐部线下面临倒闭?

在2024年的中国&#xff0c;智慧竞技游戏俱乐部如雨后春笋般在二三线城市中兴起&#xff0c;它们不仅是年轻人娱乐的场所&#xff0c;更是智慧与技巧的较量场。然而&#xff0c;随着疫情的冲击&#xff0c;这些俱乐部面临着前所未有的挑战。本文将通过一个小镇上的故事&#xf…...

jmeter分布式(四)

一、gui jmeter的gui主要用来调试脚本 1、先gui创建脚本 先做一个脚本 演示&#xff1a;如何做混合场景的脚本&#xff1f; 用211的业务比例 ①启动数据库服务 数据库服务&#xff1a;包括mysql、redis mysql端口默认3306 netstat -lntp | grep 3306处于监听状态&#xf…...

如何解决手机游戏因IP代理被封禁无法正常游戏的问题?

在当前的网络环境下&#xff0c;许多手机游戏为了维护游戏的公平性和安全性&#xff0c;会采取措施对使用IP代理的玩家进行封禁&#xff0c;导致他们无法正常访问游戏。这种情况对于一些需要使用IP代理的用户来说可能显得很棘手&#xff0c;但实际上有几种技术性的解决方案可以…...

windows10 安装Anaconda

文章目录 1. 下载2. 安装3. 配置环境变量4. 检查是否安装成功 1. 下载 官网下载 https://www.anaconda.com/download 下载的最新版本&#xff0c;要求python的版本也高一些 清华大学开源软件镜像站 https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/ 所有版本在这个网…...

[图解]SysML和EA建模住宅安全系统-14-黑盒系统规约

1 00:00:02,320 --> 00:00:07,610 接下来&#xff0c;我们看下一步指定黑盒系统需求 2 00:00:08,790 --> 00:00:10,490 就是说&#xff0c;把这个系统 3 00:00:11,880 --> 00:00:15,810 我们的目标系统&#xff0c;ESS&#xff0c;看成黑盒 4 00:00:18,030 --> …...

frp内网穿透xtcp安全点对点p2p部署记录打洞失败解决方法

环境 一、有公网IP、nas主机&#xff08;需要穿透里面的服务&#xff09;、安卓手机、frps-0.58.1、frpc-0.59.0(群晖NAS套件)、安卓版frpc-0.56.0 二、两端frpc必须要有一端nat网络类型不是非对称nat 开始 有公网的主机上配置frps.toml bindPort 7000nas主机端frpc.toml配…...

C++基础篇(2)

目录 前言 1.缺省参数 2.函数重载 2.1函数重载的基本规则 ​编辑2.2注意事项 2.3 重载解析&#xff08;Overload Resolution&#xff09;--补充内容 3.引用 3.1引用的概念和定义 3.2引用的特性 3.3引用的使用 3.4const引用 4.指针和引用的关系 结束语 前言 上节小编…...

c++ primer plus 第16章string 类和标准模板库,16.1.3 使用字符串

c primer plus 第16章string 类和标准模板库,16.1.3 使用字符串 c primer plus 第16章string 类和标准模板库,16.1.3 使用字符串 文章目录 c primer plus 第16章string 类和标准模板库,16.1.3 使用字符串16.1.3 使用字符串程序清单16.3 hangman.cpp 16.1.3 使用字符串 现在&a…...

使用mybatis的statementHander拦截器监控表和字段并发送钉钉消息

新建mybatis的statementHander拦截器拦截器 类 面试题&#xff1a; 2.实现 解析Sql时引入JSqlParser JSqlParser 是一个 SQL 语句解析器。 它将 SQL转换为可遍历的 Java 类层次结构。 <dependency><groupId>com.github.jsqlparser</groupId><artifac…...

信贷系统——基础信贷概念

摘要 信贷是金融领域中的一个重要概念,指的是金融机构(如银行、信用合作社等)向个人、企业或政府提供资金的过程。在信贷过程中,金融机构向借款人提供资金,借款人则承诺在未来的某个时间点按照约定的条件和利率偿还借款。这种借款通常是在合同中明确约定的,包括贷款金额、…...

分页查询及其拓展应用案例

分页查询 分页查询是处理大量数据时常用的技术&#xff0c;通过分页可以将数据分成多个小部分&#xff0c;方便用户逐页查看。SQLAlchemy 提供了简单易用的方法来实现分页查询。 本篇我们也会在最终实现这样的分页效果&#xff1a; 1. 什么是分页查询 分页查询是将查询结果按照…...

【UE5.1】NPC人工智能——02 NPC移动到指定位置

效果 步骤 1. 新建一个蓝图&#xff0c;父类选择“AI控制器” 这里命名为“BP_NPC_AIController”&#xff0c;表示专门用于控制NPC的AI控制器 2. 找到我们之前创建的所有NPC的父类“BP_NPC” 打开“BP_NPC”&#xff0c;在类默认值中&#xff0c;将“AI控制器类”一项设置为“…...

有关电力电子技术的一些相关仿真和分析:⑤交-直-交全桥逆变+全波整流结构电路(MATLAB/Siumlink仿真)

全桥逆变+全波整流结构 参数:Vin=500V, Vo=200V, T=2:1:1, RL=10Ω, fs=100kHz, L=1mH, C=100uF (1)给定输入电压,输出电压和主电路参数,仿真研究电路工作原理,分析工作时序; (2)调节负载电阻,实现电流连续和断续,并仿真验证; (3)调节占空比,分析占空比与电…...

记录一次Android推流、录像踩坑过程

背景&#xff1a; 按照需求&#xff0c;需要支持APP在手机息屏时进行推流、录像。 技术要点&#xff1a; 1、手机在息屏时能够打开camera获取预览数据 2、获取预览数据时进行编码以及合成视频 一、息屏时获取camera预览数据&#xff1a; ①Camera.setPreviewDisplay(SurfaceH…...

VsCode 与远程服务器 ssh免密登录

首先配置信息 加入下列信息 Host qb-zn HostName 8.1xxx.2xx.3xx User root ForwardAgent yes Port 22 IdentityFile ~/.ssh/id_rsa 找到自己的公钥&#xff0c;不带pub是私钥&#xff0c;打死都不能给别人。复制公钥 拿到公钥后&#xff0c;来到远程服务器 vim ~/.ss…...

7/13 - 7/15

vo.setId(rs.getLong("id"))什么意思&#xff1f; vo.setId(rs.getLong("id")); 这行代码是在Java中使用ResultSet对象&#xff08;通常用于从数据库中检索数据&#xff09;获取一个名为"id"的列&#xff0c;并将其作为long类型设置为一个对象…...

烟雾监测与太阳能源:实验装置在其中的作用

太阳光在烟雾中的散射效应研究实验装置是一款模拟阳光透过烟雾环境的设备。此装置能帮助探究阳光在烟雾中的传播特性、散射特性及其对阳光的影响。 该装置主要包括光源单元、烟雾发生装置、光学组件、以及系统。光源单元负责产生类似于太阳光的光线&#xff0c;通常选用高亮度的…...

QT下,如何获取控制台输入

最近工作中为了测试某个模块&#xff0c;需要把原先输入模块部分&#xff0c;改成控制台输入来方便测试。在QT中&#xff0c;我们可以使用 QTextStream 类来读取用户的输入来达到目的。下面是一个简单的例子&#xff1a; #include <QCoreApplication> #include <QTex…...

mybatis动态传入参数 pgsql 日期 Interval ,day,minute

mybatis动态传入参数 pgsql 日期 Interval 在navicat中&#xff0c;标准写法 SELECT * FROM test WHERE time > (NOW() - INTERVAL 5 day)在mybatis中&#xff0c;错误写法 SELECT * FROM test WHERE time > (NOW() - INTERVAL#{numbers,jdbcTypeINTEGER} day)报错内…...

常见CSS属性

常见CSS属性。 1. display: 定义&#xff1a;display 属性控制元素如何渲染在文档流中&#xff0c;影响了元素是否占用空间、位置及盒子模型的行为。 使用说明&#xff1a;它可以设置为如block, inline, inline-block, flex, grid, none等值&#xff0c;用于决定元素显示模式…...

WSL-Ubuntu20.04训练环境配置

1.YOLOv8训练环境配置 训练环境配置的话就仍然以YOLOv8为例&#xff0c;来说明如何配置深度学习训练环境。这部分内容比较简单&#xff0c;主要是安装miniAnaconda以及安装torch和torchvision. 首先是miniAnaconda的安装(参考官网的教程Miniconda — Anaconda )&#xff0c;执行…...

运维检查:mysql表自增id是否快要用完

数据库表中最大自增ID用完会报错。判断是否接近或达到自增ID类型的最大值&#xff1a;‌ 对于MySQL中的自增ID&#xff0c;‌如果使用的是int类型&#xff0c;‌其无符号&#xff08;‌unsigned&#xff09;‌的最大值可以达到2^32 - 1&#xff0c;‌即4294967295。‌如果使用的…...

深入理解FFmpeg--libavformat接口使用(一)

libavformat&#xff08;lavf&#xff09;是一个用于处理各种媒体容器格式的库。它的主要两个目的是去复用&#xff08;即将媒体文件拆分为组件流&#xff09;和复用的反向过程&#xff08;以指定的容器格式写入提供的数据&#xff09;。它还有一个I/O模块&#xff0c;支持多种…...