C语言——网络编程(下)

目录

1、UDP

1.1、UDP的特点

1.2、UDP的使用场景

1.3、UDP的优缺

1.4、UDP的实现

1.5示例

2.IO模型

2.1、I/O多路复用

2.2、多路复用select()的实现

2.3、多路复用poll()的实现

2.4、select()与poll()的区别

3、套接字属性

3.1、套接字属性查看与修改

3.1.1、SOL_SOCKET

3.1.2、IPPROTO_IP

3.1.3、IPPRO_TCP

3.1.4、常见的错误


1、UDP

UDP(User Datagram Protocol)是一种不可靠的传输层协议,用于在IP网络中传输数据包。

1.1、UDP的特点

  • 不可靠:UDP不保证数据包的到达和顺序,数据可能会丢失、 corruption 或重复。
  • 无连接:UDP不需要建立连接,而是每个数据包单独地传输。
  • 无序序:UDP不保证数据包的顺序,可能会出现乱序。
  • 无确认机制:UDP不需要确认数据包的到达。

1.2、UDP的使用场景

  • 实时应用:UDP通常用于实时应用,如视频流、音频流、游戏等,这些应用需要快速传输数据,而不关心数据的可靠性。
  • 小数据包传输:UDP适用于小数据包的传输,如DNS查询、DHCP分配等,这些应用不需要传输大量数据。
  • 网关设备:UDP通常用于网关设备的数据传输,如路由器、交换机等,这些设备需要快速传输数据,而不关心数据的可靠性。

1.3、UDP的优缺

优点:

  • 高速传输:UDP的传输速度快,能够快速传输数据。
    *低延迟:UDP的传输延迟低,能够实时传输数据。

缺点:

  • 不可靠:UDP的数据传输不可靠,可能会丢失、 corruption 或重复。
  • 无确认机制:UDP没有确认机制,无法确定数据是否到达目的地。

1.4、UDP的实现

UDP的实现主要涉及到以下几个方面:

  • UDP头:UDP头包含源端口号、目的端口号、数据长度和检验和。
  • 数据传输:UDP将数据分割成小包,添加头信息,然后传输到目的地。
  • 接收端处理:接收端将收到的数据包组装成原始数据,并检查是否完整和正确。

总的来说,UDP是一种不可靠的传输协议,适用于实时应用和小数据包传输。但是,它的不可靠性和缺少确认机制也使得它在某些情况下不可用。

1.5示例

服务器端

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8888
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE];

    // 创建UDP套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 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);

    // 绑定套接字到地址
    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    printf("UDP server started on port %d", PORT);

    while (1) {
        // 接收数据
        ssize_t n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, (struct sockaddr *)&client_addr, &client_len);
        if (n < 0) {
            perror("recvfrom failed");
            continue;
        }
        buffer[n] = '\0'; // 添加字符串结束符

        printf("Received from %s:%d: %s", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buffer);

        // 发送回显
        sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&client_addr, client_len);
    }

    close(sockfd);
    return 0;
}

客户端

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERVER_IP "127.0.0.1" // 服务器IP地址
#define SERVER_PORT 8888
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE];

    // 创建UDP套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 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 = inet_addr(SERVER_IP);
    server_addr.sin_port = htons(SERVER_PORT);

    printf("Enter message: ");
    fgets(buffer, BUFFER_SIZE, stdin);
    buffer[strcspn(buffer, "")] = 0; // 去除换行符

    // 发送数据
    if (sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("sendto failed");
        exit(EXIT_FAILURE);
    }

    // 接收数据
    ssize_t n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, NULL, NULL);
    if (n < 0) {
        perror("recvfrom failed");
        exit(EXIT_FAILURE);
    }
    buffer[n] = '\0';

    printf("Received from server: %s", buffer);

    close(sockfd);
    return 0;
}

2.IO模型

IO模型是操作系统中一种重要的概念,用于描述进程或线程与IO设备之间的交互方式。常见的IO模型有以下四种:

  1. 阻塞I/O(Blocking I/O)

    在阻塞I/O模型中,进程或线程在等待IO操作完成时将被阻塞,直到IO操作完成后才继续执行。

  2. 非阻塞I/O(Non-Blocking I/O)

    在非阻塞I/O模型中,进程或线程在等待IO操作完成时不会被阻塞,可以继续执行其他任务。

  3. I/O多路复用(I/O Multiplexing)

    在I/O多路复用模型中,进程或线程可以同时监控多个IO设备的状态,而不需要阻塞或轮询每个设备。

  4. 异步I/O(Asynchronous I/O)

    在异步I/O模型中,IO操作将在后台执行,而进程或线程可以继续执行其他任务,不需要等待IO操作完成。

2.1、I/O多路复用

I/O多路复用(I/O Multiplexing)是一种技术,允许单个进程监控多个文件描述符(File Descriptor),并在其中的一个或多个文件描述符上进行I/O操作,而不需要创建多个进程或线程。该技术可以提高系统的并发性和效率。

I/O多路复用有以下几个主要技术:

  1. select()函数:该函数监控多个文件描述符,并返回哪些文件描述符已经就绪可以进行I/O操作。
  2. poll()函数:该函数监控多个文件描述符,并返回哪些文件描述符已经就绪可以进行I/O操作。
  3. epoll()函数(Linux特有):该函数监控多个文件描述符,并返回哪些文件描述符已经就绪可以进行I/O操作。

在使用I/O多路复用时,需要完成以下步骤:

  1. 创建文件描述符数组,用于存储需要监控的文件描述符。
  2. 使用select()、poll()或epoll()函数监控文件描述符数组,并返回哪些文件描述符已经就绪可以进行I/O操作。
  3. 对于就绪的文件描述符,进行I/O操作。
  4. 重复步骤2和3,直到所有文件描述符都已经处理完毕。

2.2、多路复用select()的实现

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>

#define PORT 8080
#define MAX_CLIENTS 10

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    int max_fd = 0;
    fd_set read_fds;
    char buffer[256];

    // 创建服务器套接字
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket");
        exit(1);
    }

    // 设置服务器套接字地址和端口
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

    // 绑定服务器套接字
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind");
        exit(1);
    }

    // 监听客户端连接
    if (listen(server_fd, MAX_CLIENTS) < 0) {
        perror("listen");
        exit(1);
    }

    while (1) {
        // 创建文件描述符集
        FD_ZERO(&read_fds);
        FD_SET(server_fd, &read_fds);
        max_fd = server_fd;

        // 等待客户端连接
        if (select(max_fd + 1, &read_fds, NULL, NULL, NULL) < 0) {
            perror("select");
            exit(1);
        }

        // 处理客户端连接
        if (FD_ISSET(server_fd, &read_fds)) {
            client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
            if (client_fd < 0) {
                perror("accept");
                exit(1);
            }

            printf("Connected by client IP address %s and port %d",inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

            // 将客户端文件描述符添加到文件描述符集中
            FD_SET(client_fd, &read_fds);
            if (client_fd > max_fd) {
                max_fd = client_fd;
            }
        }

        // 处理客户端数据
        for (int i = server_fd; i <= max_fd; i++) {
            if (FD_ISSET(i, &read_fds)) {
                if (i == server_fd) {
                    // 处理客户端连接
                } else {
                    // 读取客户端数据
                    read(i, buffer, 256);
                    printf("Received from client %s:%d: %s",inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buffer);
                }
            }
        }
    }

    return 0;
}

2.3、多路复用poll()的实现

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <poll.h>

#define PORT 8080
#define MAX_CLIENTS 10

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    int client_fds[MAX_CLIENTS];
    int client_fds_count = 0;
    struct pollfd poll_fds[MAX_CLIENTS];

    // 创建服务器 socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket");
        return -1;
    }

    // 设置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    // 绑定服务器 socket
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind");
        return -1;
    }

    // 监听客户端连接
    if (listen(server_fd, MAX_CLIENTS) < 0) {
        perror("listen");
        return -1;
    }

    while (1) {
        // 创建 poll 结构体
        for (int i = 0; i < MAX_CLIENTS; i++) {
            poll_fds[i].fd = -1;
            poll_fds[i].events = POLLIN;
        }

        // 读取可读的客户端 socket
        for (int i = 0; i < client_fds_count; i++) {
            poll_fds[i].fd = client_fds[i];
        }

        // 进行 poll
        int ret = poll(poll_fds, client_fds_count, -1);
        if (ret < 0) {
            perror("poll");
            return -1;
        }

        // 处理 poll 事件
        for (int i = 0; i < client_fds_count; i++) {
            if (poll_fds[i].revents & POLLIN) {
                // 可读客户端 socket
                client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
                if (client_fd < 0) {
                    perror("accept");
                    return -1;
                }

                // 添加客户端 socket 到数组中
                client_fds[client_fds_count++] = client_fd;
            }
        }
    }

    return 0;
}

2.4、select()与poll()的区别

pollselect都是I/O多路复用的系统调用,它们的主要区别在于:

  1. 文件描述符的表示方式: select使用三个位图(readfdswritefdsexceptfds)来表示关注的文件描述符集合,每个位图的大小固定,最大只能处理FD_SETSIZE个文件描述符(通常是1024)。而poll使用一个pollfd结构体的数组来表示关注的文件描述符集合,每个pollfd结构体包含一个文件描述符和事件掩码,理论上可以处理的文件描述符数量不受限制,只受限于系统内存。 因此,poll在处理大量文件描述符时具有更高的效率和可扩展性。

  2. 事件通知方式: select返回后,需要遍历三个位图来确定哪些文件描述符就绪,效率较低。poll返回后,可以直接从pollfd数组中获取就绪的文件描述符及其事件,效率更高。

  3. 出错处理: selectpoll在出错时都会返回-1,并设置errno。 但是poll的出错处理相对更清晰,因为pollfd数组中的每个元素都能独立地反映其状态,方便定位错误。

总而言之,poll相较于select,在处理文件描述符数量和效率上都有改进,但两者本质上都是基于轮询的机制,都需要内核遍历文件描述符集合,因此在处理大量文件描述符时,性能仍然会成为瓶颈。 epoll作为更高效的I/O多路复用机制,在实际应用中更受欢迎。

3、套接字属性

  • 地址族(Address Family): 指定套接字使用的网络协议族,例如AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX(Unix域套接字)。 这决定了套接字能够连接到哪种类型的网络。

  • 套接字类型(Socket Type): 定义套接字的通信方式,例如:

    • SOCK_STREAM:面向连接的可靠传输,例如TCP。 数据有序可靠地到达,保证数据完整性。
    • SOCK_DGRAM:面向无连接的不可靠传输,例如UDP。 数据包独立传输,可能丢失、乱序或重复。
    • SOCK_RAW:原始套接字,允许访问网络协议栈的底层,通常用于网络编程的底层操作,例如网络监控和数据包分析。
  • 协议(Protocol): 指定使用的网络协议,例如IPPROTO_TCP、IPPROTO_UDP。 通常由套接字类型隐式决定,但有些情况下可以明确指定。

  • 缓冲区大小(Buffer Size): 发送和接收数据的缓冲区大小,可以影响性能。 过小可能导致频繁的I/O操作,过大可能浪费内存。

  • 连接状态(Connection State): 描述套接字的连接状态,例如已连接、监听、已关闭等。 这对于管理连接至关重要。

  • 选项(Options): 可以设置各种选项来控制套接字的行为,例如:

    • SO_REUSEADDR:允许重用本地地址和端口。
    • SO_LINGER:控制关闭套接字时的行为。
    • SO_SNDBUFSO_RCVBUF:设置发送和接收缓冲区大小。
    • SO_KEEPALIVE:启用心跳机制,检测连接是否仍然有效。

这些属性共同定义了套接字的特性和行为,决定了它如何参与网络通信。 通过设置不同的属性,可以创建不同类型的套接字以满足不同的网络编程需求。

3.1、套接字属性查看与修改

int getsockopt(int sockfd, int level, int optname,void *optval, socklen_t *optlen);

int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);

cpp 复制代码
参数:
	sockfd:套接字
    level:设置属性层
        SOL_SOCKET:通用套接字层
        IPPROTO_IP:IP层
    	IPPRO_TCP:TCP层
    optname:指定操作,一般用宏表示
    optval:设置属性对应的值
    optlen:设置属性对应值长度
        
返回值:
	成功返回0,失败返回-1

3.1.1、SOL_SOCKET

cpp 复制代码
optname                                    optval类型
**SO_BROADCAST**    允许发送广播数据            int            
SO_DEBUG            允许调试                    int            
SO_DONTROUTE        不查找路由                  int            
SO_ERROR            获得套接字错误              int            
SO_KEEPALIVE        保持连接                    int            
SO_LINGER           延迟关闭连接             struct linger  
SO_OOBINLINE        带外数据放入正常数据流       int            
SO_RCVBUF           接收缓冲区大小               int            
SO_SNDBUF           发送缓冲区大小               int            
SO_RCVLOWAT         接收缓冲区下限               int            
SO_SNDLOWAT         发送缓冲区下限               int            
SO_RCVTIMEO         接收超时                 struct timeval 
SO_SNDTIMEO         发送超时                 struct timeval 
**SO_REUSEADDR**    允许重用本地地址和端口       int            
SO_TYPE             获得套接字类型               int            
SO_BSDCOMPAT        与BSD系统兼容                int            

3.1.2、IPPROTO_IP

cpp 复制代码
optname                                               optval类型     
IP_ADD_MEMBERSHIP        将指定IP加入到组播组中        struct ip_mreq 
IP_MULTICAST_IF          允许开启组播报文的接口        struct ip_mreq 

3.1.3、IPPRO_TCP

cpp 复制代码
optname                              optval类型 
TCP_MAXSEG     TCP最大数据段的大小     int        
TCP_NODELAY    不使用Nagle算法         int        

3.1.4、常见的错误

bind failed: Address already in use

错误原因:上次连接的TCP还没有完全断开(涉及tcp的分手过程),端口被占用

解决方式:1、显示的是地址错误,其实修改端口号就可以了,但是多次的话比较麻烦,可以采用 第二种方式,永绝后患

2、int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen); 调用该函数,将SOL_SOCKET 这个参数设置为**SO_REUSEADDR** 允许重用本 地地址和端口。

4、tcp三次握手和四次挥手

三次握手和四次挥手是TCP连接建立和关闭过程中使用的机制,保证了可靠的数据传输。

三次握手 (Three-way handshake): 用于建立TCP连接。

  1. SYN (同步): 客户端向服务器发送一个SYN包,其中包含客户端选择的初始序列号(ISN)。这个包表示客户端希望建立连接。

  2. SYN-ACK (同步-确认): 服务器收到SYN包后,向客户端发送一个SYN-ACK包。这个包包含服务器选择的初始序列号(ISN)以及对客户端SYN包的确认号(ACK),确认号等于客户端的ISN加1。

  3. ACK (确认): 客户端收到SYN-ACK包后,向服务器发送一个ACK包。这个包确认收到了服务器的SYN-ACK包,并包含服务器ISN加1的确认号。 至此,连接建立成功。

四次挥手 (Four-way handshake): 用于关闭TCP连接。

  1. FIN (结束): 客户端向服务器发送一个FIN包,表示客户端不再发送数据,但仍可以接收数据。这个包包含客户端的序列号。

  2. ACK (确认): 服务器收到FIN包后,向客户端发送一个ACK包,确认收到了客户端的FIN包。这个包包含客户端序列号加1的确认号。 注意,服务器此时可能仍然有数据要发送给客户端。

  3. FIN (结束): 服务器发送完所有数据后,向客户端发送一个FIN包,表示服务器也不再发送数据。这个包包含服务器的序列号。

  4. ACK (确认): 客户端收到服务器的FIN包后,向服务器发送一个ACK包,确认收到了服务器的FIN包。这个包包含服务器序列号加1的确认号。 至此,连接关闭。

三次握手和四次挥手区别的根本原因在于TCP连接是全双工的。 客户端和服务器都可以同时发送和接收数据。 关闭连接需要分别处理客户端和服务器的数据发送方向,因此需要四次挥手来确保双方都正确地关闭连接。 如果只有三次挥手,则无法保证服务器已经发送完所有数据。

相关推荐
ZZZCY200342 分钟前
华为ENSP--IP编址及静态路由配置
网络·华为
EasyCVR1 小时前
私有化部署视频平台EasyCVR宇视设备视频平台如何构建视频联网平台及升级视频转码业务?
大数据·网络·音视频·h.265
hgdlip1 小时前
主IP地址与从IP地址:深入解析与应用探讨
网络·网络协议·tcp/ip
珹洺2 小时前
C语言数据结构——详细讲解 双链表
c语言·开发语言·网络·数据结构·c++·算法·leetcode
科技象限2 小时前
电脑禁用U盘的四种简单方法(电脑怎么阻止u盘使用)
大数据·网络·电脑
东方隐侠安全团队-千里2 小时前
网安瞭望台第3期:俄黑客 TAG - 110组织与密码攻击手段分享
网络·chrome·web安全·网络安全
云计算DevOps-韩老师2 小时前
【网络云计算】2024第47周-每日【2024/11/21】周考-实操题-RAID6实操解析2
网络·云计算
耗同学一米八3 小时前
2024 年河北省职业院校技能大赛网络建设与运维赛项样题四
运维·网络
速盾cdn3 小时前
速盾:CDN缓存的工作原理是什么?
网络·安全·web安全