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 <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>int main()
{//创建socket//int socket(int domain, int type, int protocol);int lfd = socket(AF_INET, SOCK_STREAM, 0);if(lfd<0){perror("socket error");return -1;}//int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);//绑定struct sockaddr_in serv;bzero(&serv, sizeof(serv));serv.sin_family = AF_INET;serv.sin_port = htons(8888);serv.sin_addr.s_addr = htonl(INADDR_ANY); //表示使用本地任意可用IPint ret = bind(lfd, (struct sockaddr *)&serv, sizeof(serv));if(ret<0){perror("bind error");	return -1;}//监听//int listen(int sockfd, int backlog);listen(lfd, 3);//int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);struct sockaddr_in client;socklen_t len = sizeof(client);int cfd = accept(lfd, (struct sockaddr *)&client, &len);  //len是一个输入输出参数//const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);//获取client端的IP和端口char sIP[16];memset(sIP, 0x00, sizeof(sIP));printf("client-->IP:[%s],PORT:[%d]\n", inet_ntop(AF_INET, &client.sin_addr.s_addr, sIP, sizeof(sIP)), ntohs(client.sin_port));printf("lfd==[%d], cfd==[%d]\n", lfd, cfd);int i = 0;int n = 0;char buf[1024];while(1){//读数据memset(buf, 0x00, sizeof(buf));n = read(cfd, buf, sizeof(buf));if(n<=0){printf("read error or client close, n==[%d]\n", n);break;}printf("n==[%d], buf==[%s]\n", n, buf);	for(i=0; i<n; i++){buf[i] = toupper(buf[i]);}//发送数据write(cfd, buf, n);}//关闭监听文件描述符和通信文件描述符close(lfd);close(cfd);return 0;
}
//客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>int main()
{//创建socket---用于和服务端进行通信int cfd = socket(AF_INET, SOCK_STREAM, 0);if(cfd<0){perror("socket error");return -1;}//连接服务端//int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);struct sockaddr_in serv;serv.sin_family = AF_INET;serv.sin_port = htons(8888);inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);printf("[%x]\n", serv.sin_addr.s_addr);int ret = connect(cfd, (struct sockaddr *)&serv, sizeof(serv));if(ret<0){perror("connect error");return -1;}	int n = 0;char buf[256];while(1){//读标准输入数据memset(buf, 0x00, sizeof(buf));n = read(STDIN_FILENO, buf, sizeof(buf));//发送数据write(cfd, buf, n);//读服务端发来的数据memset(buf, 0x00, sizeof(buf));n = read(cfd, buf, sizeof(buf));if(n<=0){printf("read error or server closed, n==[%d]\n", n);break;}printf("n==[%d], buf==[%s]\n", n, buf);}//关闭套接字cfdclose(cfd);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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/872632.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

集群服务器如何解决跨服务器通信?大量并发通信问题?

Nginx tcp负载均衡模块&#xff1a; 1.将client的请求按照 负载均衡算法 分发到服务器 2.负载均衡器与服务器保持心跳机制&#xff0c;监测故障、保障服务可靠性 3.可以发现添加新的服务器&#xff0c;方便扩展服务器集群的数量 Nginx反向代理用途&#xff1a; 2.4 用途 …

在golang中Sprintf和Printf 的区别

最近一直在学习golang这个编程语言&#xff0c;我们这里做一个笔记就是 Sprintf和Printf 的区别 fmt.Sprintf 根据格式化参数生成格式化的字符串并返回该字符串。 fmt.Printf 根据格式化参数生成格式化的字符串并写入标准输出。由上面就可以知道&#xff0c;fmt.Sprintf返回的…

php随机海量高清壁纸系统源码,数据采集于网络,使用很方便

2022 多个分类随机海量高清壁纸系统源码&#xff0c;核心文件就两个&#xff0c;php文件负责采集&#xff0c;html负责显示&#xff0c;很简单。做流量工具还是不错的。 非第三方接口&#xff0c;图片数据采集壁纸多多官方所有数据&#xff01; 大家拿去自行研究哈&#xff0…

在 Windows 上开发.NET MAUI 应用_2.生成你的第一个应用

先决条件 Visual Studio 2022 17.8 或更高版本&#xff0c;并安装了 .NET Multi-platform App UI 工作负载。 可参考上一篇文章&#xff1a;http://t.csdnimg.cn/n38Yy 创建应用 1.启动 Visual Studio 2022。 在开始窗口中&#xff0c;单击“创建新项目”以创建新项目&#…

【B树、B-树、B+、B*树】

目录 一、B-树&#xff08;即B树&#xff09;的定义及操作1.1、定义1.2、操作1.2.1、查找1.2.2、插入1.2.3、删除 二、B树的定义及操作2.1、定义2.2、操作2.2.1、查找2.2.2、插入2.2.3、删除 三、B*树 一、B-树&#xff08;即B树&#xff09;的定义及操作 1.1、定义 B-tree即…

【c++11】什么情况下需要封装set/get

文章目录 一、平凡类型与非平凡类型什么时候使用set/get1.平凡类型2.非平凡类型 二、构造函数参数较多解决办法1.把所有参数放到一个结构体里面2.使用build设计模式 三、如果构造函数众多&#xff08;参数很多&#xff09;1.模仿make_unique&#xff0c;就地构造2.基于build设计…

Missing script:‘dev‘

场景&#xff1a; npm run dev 原因&#xff1a;没有安装依赖&#xff0c;可用镜像安装&#xff08;详见下图ReadMe 蓝色字体&#xff09;&#xff0c;没安装依赖可从package-lock.json文件是否存在看出&#xff0c;存在则有依赖 解决&#xff1a;

二叉树、B树/B-树

二叉树 在中文语境中,节点结点傻傻分不清楚,故后文以 node 代表 "结点",root node 代表根节点,child node 代表 “子节点” 二叉树是诸多树状结构的始祖,至于为什么不是三叉树,四叉树,或许是因为计算机只能数到二吧,哈哈,开个玩笑。二叉树很简单,每个 no…

useState函数

seState是一个react Hook(函数)&#xff0c;它允许我们像组件添加一个状态变量&#xff0c;从而控制影响组件的渲染结果 数据驱动试图 本质&#xff1a;和普通JS变量不同的是&#xff0c;状态变量一旦发生变化组件的视图UI也会随着变化(数据驱动试图) 使用 修改状态 注意&am…

单链表算法 - 链表分割

链表分割_牛客题霸_牛客网现有一链表的头指针 ListNode* pHead&#xff0c;给一定值x&#xff0c;编写一段代码将所有小于x的。题目来自【牛客题霸】https://www.nowcoder.com/practice/0e27e0b064de4eacac178676ef9c9d70思路: 代码: /* struct ListNode {int val;struct List…

英福康INFICON TranspectorWare v3 RGA软件操作说明

英福康INFICON TranspectorWare v3 RGA软件操作说明

Python一对一辅导答疑|Rust 德国

你好&#xff0c;我是悦创。 下面是答疑内容。 在 Rust 中&#xff0c;方法的调用方式通常取决于它们是如何定义的。在你的例子中&#xff0c;print_drink方法最初是作为一个接受Drink类型实例作为参数的关联函数&#xff08;类似于静态方法&#xff09;定义的。后来&#xff…

供应链管理(SCM):如何在颜值和体验上发力

要在供应链管理系统&#xff08;SCM&#xff09;中在颜值和体验上发力&#xff0c;让用户感觉耳目一新&#xff0c;可以采取以下措施&#xff1a; 界面设计优化&#xff1a; 对供应链管理系统的界面进行优化&#xff0c;注重界面的美观、简洁和易用性。采用现代化的设计风格、…

技能 | postman接口测试工具安装及使用

哈喽小伙伴们大家好!今天来给大家分享一款轻量级,高效好用的接口测试工具-postman. Postman是一个流行的API开发工具&#xff0c;主要用于测试、开发和文档化API。以下是关于Postman的介绍及其主要使用场景&#xff1a; Postman介绍&#xff1a; 1. 功能丰富的API客户端&#…

在SpringCloud中如何轻松实现微服务间的通信

在Spring Cloud中&#xff0c;实现微服务间的通信非常简单。Spring Cloud提供了多种方式来进行微服务之间的通信&#xff0c;包括使用RestTemplate、Feign、Ribbon、Eureka等组件。下面我将详细介绍这些方式的使用方法。 使用RestTemplate进行通信&#xff1a; RestTemplate是S…

django报错(一):python manage.py makemigrations,显示“No changes detected”

执行python manage.py makemigrations命令无任何文件生成&#xff0c;结果显示“No changes detected”。 解决方案一&#xff1a; 1、执行命令&#xff1a;python manage.py makemigrations –empty appname 2、删除其中的0001_initial.py文件&#xff08;因为这个文件内容是…

【docker 部署springboot项目】

一、docker安装 1.检查Linux内核版本高于3.10才可安装 uname -r 2. 卸载旧版本 sudo yum remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine 3. 使用docker仓库进行安装 安装所需的软…

Qt MV架构-委托类

一、基本概念 与MVC模式不同&#xff0c;MV视图架构中没有包含一个完全分离的组件来处理与用户的交互。 一般地&#xff0c;视图用来将模型中的数据显示给用户&#xff0c;也用来处理用户的输入。为了获得更高的灵活性&#xff0c;交互可以由委托来执行。 这些组件提供了输入…

Python入门------pycharm加载虚拟环境

pycharm虚拟环境配置&#xff1a; 在按照前面的办法&#xff0c;配置好虚拟环境后,如果我们需要到虚拟环境开发&#xff0c;就需要给编译器配置虚拟环境 1.打开编译器&#xff0c;点击右下角的interpreter选项 2. 点击ADD Interpreter,添加虚拟环境 3. 因为我们使用的是原始…

欧式空间、傅里叶级数与希尔伯特空间的解释

欧式空间&#xff08;欧几里得空间&#xff09; 欧几里得几何就是中学学的平面几何、立体几何&#xff0c;在欧几里得几何中&#xff0c;两平行线任何位置的间距相等。 而中学学的几何空间一般是2维&#xff0c;3维&#xff08;所以&#xff0c;我们讨论余弦值、点间的距离、内…