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

news2024/11/25 20:43:02

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 1024

int 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 1024

int 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.coloradmin.cn/o/1930244.html

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

相关文章

web自动化测试selenium的基本使用

目录 初始化浏览器并打开网页 定位网页元素 定位的方法 模拟键盘操作 模拟鼠标操作 xpath方法 xpath结点 路径表达式 轴 selenium是一个很流行的自动化测试的库&#xff0c;主要用于模拟浏览器的运行&#xff0c;是web应用测试的工具。 在使用selenium时&#xff0c;…

QSpice-(4) 层次化电路设计

QSpice-(4) 层次化电路设计 在电路规模日渐庞大的现在&#xff0c;单靠一个人把整个电路做完可能还有点麻烦&#xff0c;但也不是不可能&#xff0c;还是要怀揣着那种热情把电路做好,在电路规模比较大的时候我们通常会将电路放到子电路里面去&#xff0c;这个子电路相当于C代…

数据结构——线性表(C语言实现)

写在前面&#xff1a; 在前面C语言的结构体学习中&#xff0c;我提及了链表的操作&#xff0c; 学习数据结构我认为还是需要对C语言的数组、函数、指针、结构体有一定的了解&#xff0c;不然对于结构体的代码可能很难理解&#xff0c;特别是一些书籍上面用的还是伪代码&#xf…

Python 实现股票指标计算——RSI

RSI - 相对强弱指标 1 公式 1.1 第一种计算公式 假设A为N日&#xff08;N一般取值为6、14、21&#xff09;内收盘价的正数之和 B为N日内&#xff08;N一般取值为6、14、21&#xff09;收盘价的负数之和乘以&#xff08;-1&#xff09; 这样&#xff0c;A和B均为正&#xf…

小红书笔记怎么写才能上热门?

文末领取小红书电商开店运营教程&#xff01; 小红书作为一个拥有亿级用户的社交电商平台&#xff0c;无数人通过这里分享自己的生活点滴和购物心得。 你是不是也想让自己的小红书笔记上热门&#xff0c;获得更多的曝光和关注呢&#xff1f; 今天&#xff0c;我们就来揭秘一…

2024最新MidJourney设计Logo教程+提示词原理

你是否已经看过许多别人分享的 MJ 咒语&#xff0c;却仍无法按照自己的想法画图&#xff1f;通过学习 MJ 的提示词逻辑后&#xff0c;你将能够更好地理解并创作自己的“咒语”。本文将详细拆解使用 MJ 设计 Logo 的逻辑&#xff0c;让你在阅读后即可轻松上手&#xff0c;制作出…

应急靶场(2):Windows Server 2022 - Web1

目录 一、攻击者的shell密码 二、攻击者的IP地址 三、攻击者的隐藏账户名称 四、攻击者挖矿程序的矿池域名(仅域名) 五、有实力的可以尝试着修复漏洞 下载好靶场&#xff08;前来挑战&#xff01;应急响应靶机训练-Web1&#xff09;并搭建好环境&#xff0c;无需密码直接进入靶…

[Spring] Spring Web MVC案例实战

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏: &#x1f9ca; Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 &#x1f355; Collection与…

【堆 优先队列 第k大】2551. 将珠子放入背包中

本文涉及知识点 堆 优先队列 第k大 LeetCode2551. 将珠子放入背包中 你有 k 个背包。给你一个下标从 0 开始的整数数组 weights &#xff0c;其中 weights[i] 是第 i 个珠子的重量。同时给你整数 k 。 请你按照如下规则将所有的珠子放进 k 个背包。 没有背包是空的。 如果第…

数据采集监控平台:挖掘数据价值 高效高速生产!

在当今数字化的时代&#xff0c;数据已成为企业非常宝贵的资产之一。然而&#xff0c;要充分发挥数据的潜力&#xff0c;离不开一个强大的数据采集监控平台&#xff0c;尤其是生产制造行业。它不仅是数据的收集者&#xff0c;更是洞察生产的智慧之眼&#xff0c;高效高速处理产…

韦东山嵌入式linux系列-驱动进化之路:总线设备驱动模型-课后作业

在内核源码中搜索 platform_device_register 可以得到很多驱动&#xff0c;选择 一个作为例子&#xff1a; ① 确定它的名字 ② 根据它的名字找到对应的 platform_driver ③ 进入 platform_device_register/platform_driver_register 内部&#xff0c;分析 dev 和 drv 的匹配过…

[WUSTCTF2020]level4题解 入土为安的第三天

二叉树 Practice my Data Structure code..... Typing....Struct.....char....*left....*right............emmmmm...OK! Traversal! Traversal type 1:2f0t02T{hcsiI_SwA__r7Ee} Traversal type 2:20f0Th{2tsIS_icArE}e7__w Traversal type 3: //type3(&x[22]); No w…

Ubuntu/Linux 安装ITKSnap

文章目录 1. 安装ITKSnap1.1 下载后安装 2.进入opt文件夹改名3. 更改启动项4. 创建硬链接5. 添加桌面启动方式6. 即可使用 1. 安装ITKSnap http://www.itksnap.org/pmwiki/pmwiki.php?nMain.HomePage 1.1 下载后安装 找到下载的文件夹&#xff0c;文件夹内打开terminal。复…

Windows系统中MySQL的安装和卸载(详细包含msi和zip下载方式,以及完全卸载方法,易出现问题及解决方案等)

MySQL的安装: 第一种:msi安装(交简单,但是不能自定义安装路径) 下载地址:https://dev.mysql.com/downloads/installer/ 选择历史版本 选择安装版本,这里我选择的是8.0.37的版本,然后点击Download下载离线安装包 如下图即为下载好的版本,双击打开安装 出现如下情况,…

Git操纵本地仓库和远程仓库

git是一个代码托管的平台&#xff0c;我们可以对我们的代码进行分支 推送提交 打标签等等操作&#xff0c;而且git使用过程中也是支持一些linux语言的 比如cd呀 touch mkdir啊等等等 git的具体安装过程就不再赘述 我个人认为 好多东西就是 代码也好 文字 文档 也好&…

Centos7 桌面版安装ToDesk远程控制;linux安装Todesk远程控制;linux远程桌面方案

获取安装包&#xff1a; https://dl.todesk.com/linux/todesk-v4.7.2.0-c7-x86_64.rpm 得到 这个文件就是linux的安装包&#xff0c;后缀名就是rpm&#xff0c;类似windows下的exe 将此文件以你的方式&#xff0c;放到linux中&#xff0c;放哪都行 我使用的FileZila&#xf…

新手教学系列——简单的服务配置项集中管理

前言 在开发和运维过程中&#xff0c;配置管理是一个非常重要但经常被忽视的环节。常用的配置文件格式包括env、ini和yaml等&#xff0c;它们非常适合模块级别的系统配置&#xff0c;尤其是一些敏感信息的配置&#xff0c;例如数据库连接字符串和密码等。但是&#xff0c;对于…

【系统架构设计】数据库系统(一)

数据库系统&#xff08;一&#xff09; 数据库模式与范式数据库的结构与模式数据模型关系代数数据的规范化反规范化 数据库设计事务管理备份与恢复分布式数据库系统数据仓库数据挖掘NoSQL大数据 数据库模式与范式 数据库的结构与模式 数据库技术中采用分级的方法将数据库的结…

释放DOE的能量,快速确定最佳工艺设置,节省时间、成本和资源

您是否希望降低成本、提高生产效率&#xff0c;并最大限度地减少行业对环境的影响&#xff1f; 所有行业&#xff0c;尤其是钢铁、铝、水泥和石化等能源密集型行业&#xff0c;都面临着应对这些挑战的持续压力。供应链压力、可持续发展、严格的监管环境、日益增长的消费者预期…

Transformer中高级位置编码的介绍和比较:Linear Rope、NTK、YaRN、CoPE

在处理诸如文本之类的序列时&#xff0c;排序信息显然是至关重要的。为了结合排序信息而不是将序列视为集合&#xff0c;对位置信息进行编码是至关重要的。位置编码通过为每个位置分配嵌入向量并将其添加到相应的标记表示来实现这一点。绝对和相对位置编码是最常见的两种位置编…