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 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;
}
  • 18
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值