嵌入式Linux开发学习记录(5)

一,通信方式对比分析

信号量主要用于同步多个进程的执行,而消息队列则用于传递消息。共享内存允许多个进程直接访问同一块内存区域,从而实现数据共享。套接字和管道则更多地用于数据传输。

1. 信号量

1.1 定义与工作原理

信号量是一个计数器,用于多进程同步和互斥。它是一个非负整数值,通常用于控制对共享资源的访问。当一个进程完成对资源的访问后,信号量会增加;当一个进程希望访问资源时,它会测试信号量。如果信号量为零,则进程将休眠,直到信号量不为零。正如《思考,快与慢》中所说:“我们的思维方式通常是受到我们周围环境的影响。”这与信号量的工作原理相似,进程必须等待资源可用,这反映了我们在现实生活中的思维方式,我们经常等待合适的时机或资源来完成某些任务。

1.2 性能考量 

信号量的主要性能问题在于,当多个进程尝试访问同一资源时,可能会导致竞争条件。这可能会导致进程等待时间增加,从而降低系统的整体性能。此外,内核必须将信号量加入链表,这会增加额外的处理开销。

1.3 数据量限制 

信号量本身不存储数据,它只是一个计数器。因此,它不受数据量的限制。但是,使用信号量的进程可能会受到其他系统资源的限制,例如内存或CPU。

1.4 稳定性分析 

信号量是一种成熟的同步机制,已经在多种操作系统和应用中得到广泛应用。但是,不正确的使用信号量可能会导致死锁。例如,如果两个进程都在等待对方释放资源,那么它们都可能永远等待,导致系统停止响应。正如《存在与时间》中所说:“人类的存在是时间的存在。”这与进程在等待资源时的行为相似。如果进程不正确地使用信号量,它可能会永远等待,这反映了人类在等待某事发生时可能会浪费整个生命的哲学观点。

2. 消息队列 

消息队列是一种进程间通讯(IPC)的方法,它允许多个进程或线程之间通过发送和接收消息来进行通讯。这种方式的主要优势是它提供了一种异步的通讯方式,使得发送者和接收者可以独立地工作。

2.1 定义与工作原理

消息队列(Message Queue)是一个先进先出(FIFO)的数据结构,用于存储进程间发送的消息。每个消息都有一个优先级,根据这个优先级,消息会被放入队列的适当位置。发送者进程可以将消息发送到队列中,而接收者进程可以从队列中取出消息。

正如庄子在《逍遥游》中所说:“天下之达道者,共为一家”。这意味着所有的事物都是相互联系的,就像消息队列中的消息一样,它们都在等待被处理,每个消息都有其特定的位置和意义。

2.2 性能考量 

消息队列的性能主要取决于其实现方式。在Linux中,消息队列是通过内核实现的,这意味着每次发送或接收消息时,都需要进行系统调用,这可能会导致一定的性能开销。

然而,消息队列的主要优势在于其异步性。发送者和接收者不需要同时在线,它们可以独立地工作。这种方式提供了一种高效的通讯方式,特别是在高并发的环境中。

2.3 数据量限制 

消息队列的大小通常是有限的,这意味着它只能存储有限数量的消息。当队列满时,发送者进程可能会被阻塞,直到队列中有足够的空间为止。

这种限制可能会影响到应用程序的性能,特别是在高并发的环境中。因此,设计消息队列时,需要考虑到这些因素,确保队列的大小足够大,以满足应用程序的需求。

2.4 稳定性分析 

消息队列提供了一种稳定的通讯方式。由于消息是存储在内核中的,因此即使发送者或接收者进程崩溃,消息也不会丢失。

但是,如果系统崩溃,存储在消息队列中的消息可能会丢失。因此,对于那些需要高可靠性的应用程序,可能需要考虑其他的通讯方式,或者使用持久化的消息队列。

正如孟子在《公孙丑上》中所说:“得其大者可以言其小,得其小者不可以言其大。”这意味着,只有真正理解了事物的本质,才能够看到其细节。同样,只有深入了解消息队列的工作原理,才能够充分利用其优势。

import queue
# 创建一个消息队列
q = queue.Queue()
# 向队列中添加消息
q.put("message 1")
q.put("message 2")
# 从队列中取出消息
print(q.get())  # 输出: message 1
print(q.get())  # 输出: message 2

3. 共享内存

共享内存是一种进程间通讯的方式,它允许多个进程访问同一块内存区域。这种方法的主要优势是数据传输速度快,因为数据不需要在进程之间复制。但是,它也带来了同步和数据完整性的挑战。

3.1 定义与工作原理

共享内存是一种允许多个进程访问同一块物理内存的技术。这块内存被称为“共享内存段”(Shared Memory Segment)。每个进程都可以像访问其正常的地址空间一样访问这块内存。这种方法的主要优势是它提供了一种非常快速的数据交换方式,因为数据不需要在进程之间移动或复制。

正如庄子在《逍遥游》中所说:“天地之大德曰生,而流形之大宝曰足。”这里的“流形”可以理解为数据流,而“大宝”则是我们追求的高效数据交换。共享内存就像是连接天地的桥梁,允许数据自由流动。

3.2 性能考量 

由于共享内存避免了数据复制,它通常比其他进程间通讯方法更快。但是,当多个进程需要访问同一块内存时,必须使用某种同步机制,如信号量或互斥锁,以防止数据冲突和不一致。

3.3 数据量限制

共享内存的大小受到系统的物理内存和配置限制。但是,对于大多数应用程序,这通常不是问题,因为共享内存段可以非常大。

3.4 稳定性分析 

共享内存是一种非常稳定的IPC方法。但是,如果不正确地使用(例如,没有正确地同步),它可能会导致数据不一致或其他错误。正如孟子在《公孙丑上》中所说:“得其大者可以言其小者。”在这里,“大者”指的是共享内存的广泛应用,而“小者”则是需要注意的细节和潜在的风险。

#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
int main() {
    // 使用ftok生成唯一的键
    key_t key = ftok("shmfile",65);
    // shmget返回一个标识符
    int shmid = shmget(key,1024,0666|IPC_CREAT);
    // shmat将共享内存附加到进程的地址空间
    char *str = (char*) shmat(shmid,(void*)0,0);
    printf("写入数据 : ");
    gets(str);
    printf("数据写入共享内存: %s\n",str);
    // 分离共享内存和进程
    shmdt(str);
    return 0;
}

4. 套接字 


套接字是计算机网络中用于进程间通信的端点。它们为不同计算机上的进程提供了一种双向通信的手段。套接字是网络编程的基础,支持TCP、UDP和其他多种协议。

4.1 定义与工作原理 

套接字(Socket)是一种通信机制,允许不同主机上的进程之间进行数据交换。它们是网络通信的基础,为应用程序提供了发送和接收数据的能力。正如《人类简史》中所说:“交流是人类进步的关键。”套接字为计算机提供了这种交流的能力。

import socket
# 创建一个socket对象
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接到远程服务器
s.connect(("www.example.com", 80))

4.2 性能考量


套接字的性能受到多种因素的影响,包括网络延迟、带宽限制和协议开销。TCP套接字提供了可靠的数据传输,但可能会因为握手和确认机制而导致额外的延迟。相比之下,UDP套接字提供了更快的数据传输,但不保证数据的完整性和顺序。

4.3 数据量限制


套接字本身没有固定的数据量限制,但网络带宽和协议可能会限制每次传输的数据量。例如,TCP协议会根据网络条件动态调整窗口大小,从而影响数据传输的速率。

4.4 稳定性分析 


套接字的稳定性受到网络条件、硬件限制和软件实现的影响。TCP套接字在面对网络波动时可以自动重传丢失的数据包,从而提供了较高的稳定性。然而,UDP套接字不提供这种保证,因此可能更容易受到网络不稳定的影响。

在深入探讨套接字的稳定性时,我们可以从Linux内核源码的角度进行分析。例如,tcp_v4_do_rcv函数处理接收到的TCP数据包,确保数据的完整性和顺序。

TCP客户端

import socket
# 创建一个socket对象
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接到服务器
client_socket.connect(("127.0.0.1", 12345))
# 发送数据
client_socket.sendall(b"Hello, Server!")
# 接收数据
data = client_socket.recv(1024)
print("Received:", data.decode())
# 关闭socket
client_socket.close()

TCP服务器

import socket
# 创建一个socket对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定到本地地址和端口
server_socket.bind(("127.0.0.1", 12345))
# 开始监听
server_socket.listen(5)
print("Server is listening...")
# 接受客户端连接
client_socket, addr = server_socket.accept()
print("Connected by", addr)
# 接收数据
data = client_socket.recv(1024)
print("Received:", data.decode())
# 发送数据
client_socket.sendall(b"Hello, Client!")
# 关闭socket
client_socket.close()
server_socket.close()

5. 管道


管道是Linux中最古老的进程间通信方式之一。它允许两个进程之间通过一个共同的通道进行数据交换。

5.1 定义与工作原理 


管道是一个半双工的通信方式,通常用于父子进程之间的通信。它允许一个进程的输出成为另一个进程的输入。正如庄子在《逍遥游》中所说:“天地之大德曰生”,管道就像是进程之间的生命线,使得数据能够自由流动。

#include <stdio.h>
#include <unistd.h>
int main() {
    int fd[2];
    pid_t pid;
    char buf[100];
    // 创建管道
    pipe(fd);
    if ((pid = fork()) == 0) { // 子进程
        close(fd[1]); // 关闭写端
        read(fd[0], buf, sizeof(buf)); // 从管道中读取数据
        printf("Child process received: %s\n", buf);
        close(fd[0]);
    } else { // 父进程
        close(fd[0]); // 关闭读端
        write(fd[1], "Hello from parent!", 19); // 向管道中写入数据
        close(fd[1]);
    }
    return 0;
}

5.2 性能考量


管道的性能主要受到其半双工的特性限制。这意味着在任何给定的时间点,数据只能在一个方向上流动。但是,对于小数据量的通信,管道是非常高效的。然而,当数据量增加时,可能会遇到性能瓶颈。

5.3 数据量限制


管道的数据传输量受到其缓冲区大小的限制。当缓冲区满时,写入操作会被阻塞,直到有足够的空间可用。这种限制可能会导致进程在等待数据传输时发生阻塞。

优点    适用于小数据量的传输
缺点    受缓冲区大小限制,可能导致阻塞

5.4 稳定性分析 


管道是一个非常稳定的通信方式。由于它是Linux内核的一部分,因此经过了多年的测试和优化。但是,如果不正确地使用管道,例如未正确关闭管道或在不同的上下文中使用相同的管道,可能会导致数据损坏或进程死锁。

正如孟子在《公孙丑上》中所说:“得其大者为大,得其细者为细”,在使用管道时,我们必须注意其细节,以确保数据的完整性和进程的稳定性。

6. 对比总结 


6.1 性能对比 


在Linux的进程间通信方式中,每种方法都有其独特的性能特点。为了更直观地理解各种方法的性能差异,从以下几个方面进行对比:

6.1.1 数据传输速率


信号量 (Semaphore):由于信号量主要用于同步和互斥,其数据传输速率不是其主要考虑因素。但是,信号量的操作通常是轻量级的,因此在高并发环境下表现良好。
消息队列 (Message Queue):消息队列提供了一种稳定的数据传输方式,但由于其涉及到内核空间和用户空间之间的数据复制,其速率可能受到一定的限制。
共享内存 (Shared Memory):由于数据是直接在内存中共享的,共享内存提供了最高的数据传输速率。但是,它需要额外的同步机制来确保数据的一致性。
套接字 (Sockets):套接字的数据传输速率受到网络条件的影响,但在本地通信时,如使用UNIX域套接字,其速率可以与共享内存相媲美。
管道 (Pipes):管道的数据传输速率受到其缓冲区大小的限制,但对于小数据量的通信,其速率是足够的。


6.1.2 延迟 


信号量 (Semaphore):信号量的操作延迟较低,特别是在没有竞争的情况下。
消息队列 (Message Queue):消息队列的延迟受到消息大小和队列长度的影响。
共享内存 (Shared Memory):共享内存的延迟最低,因为它避免了数据复制。
套接字 (Sockets):套接字的延迟受到网络条件和数据包大小的影响。
管道 (Pipes):管道的延迟受到其缓冲区大小和数据量的影响。
正如庄子在《逍遥游》中所说:“大知闲闲,小知间间”,在选择进程间通信方式时,我们需要根据具体的应用场景和性能需求来做出决策。

6.2 数据量对比 


在进程间通信中,数据的种类、上限、并发性和阻塞性都是关键因素。以下是各种通信方式的数据量对比:

6.2.1 数据种类


信号量 (Semaphore):主要传递的是信号或标志,不涉及具体的数据内容。
消息队列 (Message Queue):可以传递各种数据类型,包括结构体、字符串和二进制数据。
共享内存 (Shared Memory):可以共享任何数据类型,包括复杂的数据结构和大型数组。
套接字 (Sockets):主要传递字节流,但可以通过序列化和反序列化来传递复杂的数据结构。
管道 (Pipes):传递字节流,适用于文本和二进制数据。


6.2.2 数据上限


信号量 (Semaphore):不涉及数据传输,因此没有数据上限。
消息队列 (Message Queue):受到系统配置和队列设置的限制,但通常可以调整以满足需求。
共享内存 (Shared Memory):受到系统内存的限制,但提供了最大的数据传输容量。
套接字 (Sockets):数据上限主要受到网络带宽和系统资源的限制。
管道 (Pipes):受到管道缓冲区大小的限制,但可以通过系统调用来调整。


6.2.3 能否并发 


信号量 (Semaphore):主要用于同步,确保资源的互斥访问。
消息队列 (Message Queue):支持多个进程并发地读写,但可能需要额外的同步机制。
共享内存 (Shared Memory):支持并发访问,但必须使用其他同步机制,如信号量或互斥锁。
套接字 (Sockets):支持多个连接并发传输数据。
管道 (Pipes):通常只支持一个写进程和一个读进程。


6.2.4 是否阻塞


信号量 (Semaphore):操作可能会阻塞,直到资源可用。
消息队列 (Message Queue):读写操作可能会阻塞,直到有数据可用或有足够的空间。
共享内存 (Shared Memory):不会因为数据访问而阻塞,但同步操作可能会阻塞。
套接字 (Sockets):读写操作可能会阻塞,直到数据可用或连接建立。
管道 (Pipes):读操作可能会阻塞,直到有数据可用;写操作可能会阻塞,直到有足够的空间。
 

6.3 稳定性对比 


稳定性是评估进程间通信方式的关键因素。以下是各种通信方式的稳定性对比:

6.3.1 丢数据的情况 


信号量 (Semaphore):信号量本身不传输数据,因此不存在数据丢失的情况。
消息队列 (Message Queue):当队列满时,如果没有适当的错误处理,可能会导致数据丢失。
共享内存 (Shared Memory):不会因为通信方式本身导致数据丢失,但如果没有适当的同步机制,可能会导致数据不一致或覆盖。
套接字 (Sockets):在网络不稳定或连接中断的情况下,可能会丢失数据。
管道 (Pipes):当管道缓冲区满时,如果没有适当的处理,可能会导致数据丢失。


6.3.2 发送中断的处理


信号量 (Semaphore):不涉及数据传输,因此没有发送中断的问题。
消息队列 (Message Queue):可以检测到发送失败,并重新尝试或报告错误。
共享内存 (Shared Memory):不涉及数据传输,但需要确保同步操作在中断后能够正确恢复。
套接字 (Sockets):可以检测到连接中断,并重新尝试连接或报告错误。
管道 (Pipes):可以检测到写入失败,并重新尝试或报告错误。


6.3.3 能否续传 


信号量 (Semaphore):不涉及数据传输,因此没有续传的问题。
消息队列 (Message Queue):不支持续传,但可以重新发送失败的消息。
共享内存 (Shared Memory):不涉及数据传输,因此没有续传的问题。
套接字 (Sockets):支持续传,可以从上次中断的位置继续发送或接收数据。
管道 (Pipes):不支持续传,但可以重新发送失败的数据。
正如庄子在《逍遥游》中所说:“天下之达道者,共怀宇宙”,在处理进程间通信的稳定性问题时,我们需要全面考虑各种可能的情况,确保数据的完整性和通信的可靠性。

7. 结论 


在深入研究了Linux中的各种进程间通讯方式后,每种方法都有其独特的优势和局限性。选择哪种方式取决于具体的应用场景和需求。

例如,对于需要高速数据交换的应用,共享内存 (Shared Memory) 可能是最佳选择,因为它避免了数据复制,从而提供了最佳的性能。但是,如果需要跨网络的通讯,套接字 (Sockets) 则更为合适。

二,网络编程

1.TCP编程

客户端

void usage(char *s) 
{
    printf("\n%s serv_ip serv_port\n",s); 
    printf("\n\t serv_ip:server ip address"); // 提示用户输入服务器IP地址
    printf("\n\t serv_port:server port(>5000)\n\n"); // 提示用户输入服务器端口,端口号需大于5000
}

int main(int argc,char*argv[]) 
{
    int fd = -1; // 初始化套接字文件描述符为-1

    int port = -1; // 用于存放端口号,初始化为-1
    struct sockaddr_in sin; // 定义IP地址的结构体
    if(argc!=3) // 检查命令行参数是否为3个
    {
        usage(argv[0]); // 如果参数不足,则调用usage函数显示使用方法
        exit(1); // 并退出程序
    }
    // 创建socket
    if((fd=socket(AF_INET,SOCK_STREAM,0))<0) // 创建一个TCP套接字
    {
        perror("socket"); // 如果创建失败,打印错误信息
        exit(1); // 并退出程序
    }

    port = atoi(argv[2]); // 将输入的端口号字符串转换为整数
    if(port<5000) // 检查端口号是否小于5000
    {
        usage(argv[0]); // 如果小于5000,则调用usage函数显示使用方法
        exit(1); // 并退出程序
    }

    // 设置服务器地址
    bzero(&sin, sizeof(sin)); // 清零sin结构体
    sin.sin_family = AF_INET; // 设置地址族为IPv4
    sin.sin_port = htons(port); // 将主机字节序的端口号转换为网络字节序

    // 将IP地址从点分十进制转换为网络二进制形式
    if(inet_pton(AF_INET,argv[1],(void*)&sin.sin_addr.s_addr)!=1) 
    {
        perror("inet_pton"); // 如果转换失败,打印错误信息
        exit(1); // 并退出程序
    }

    // 连接到服务器
    if(connect(fd,(struct sockaddr*)&sin,sizeof(sin))<0) // 使用connect函数连接到服务器
    {
        perror("connect"); // 如果连接失败,打印错误信息
        exit(1); // 并退出程序
    }

    // 数据交换
    char buf[BUFSIZ]; // 定义一个缓冲区,用于存放数据
    while(1) // 循环以持续接收用户输入
    {
        bzero(buf, BUFSIZ); // 每次循环时清空缓冲区
        if(fgets(buf, BUFSIZ-1, stdin)==NULL) // 从标准输入读取一行
        {
            continue; // 如果读取失败,继续下一次循环
        }
        write(fd, buf, strlen(buf)); // 将读取的数据发送到服务器

        // 检查是否输入了退出字符串
        if(!strncasecmp(buf, "QUIT", strlen("QUIT"))) 
        {
            printf("Client is exiting!\n"); // 如果检测到退出命令,打印退出信息
            break; // 跳出循环
        }

    }
    // 关闭套接字
    close(fd); // 关闭与服务器的连接
    return 0; // 程序正常退出
}

服务器端


#define SERV_PORT 5001  // 定义服务器端口
#define SERV_IP_ADDR "192.168.5.10"  // 定义服务器IP地址
#define QUIT_STR "quit"  // 定义退出字符串
#define BACKLOG 5  // 定义服务器端socket可以排队的最大连接数

void cli_data_handle(void* arg)  // 声明处理客户端数据的函数,函数具体实现在后面
void sig_child_handle(int signo)  // 定义信号处理函数
{
    if (SIGCHLD == signo)
    {
        waitpid(-1, NULL, WNOHANG);  // 回收任意子进程,不阻塞
    }
}

int main(void)  // 主函数
{
    int fd = -1;  // 套接字文件描述符初始化
    struct sockaddr_in sin;  // 定义服务器的IP地址结构

    signal(SIGCHLD, sig_child_handle);  // 设置子进程退出信号的处理函数
    // 1. 创建socket
    if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        perror("socket");  // 如果创建失败,打印错误并退出
        exit(1);
    }
    // 允许绑定地址快速重用
    int b_reuse = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof(int));
    // 2. 绑定
    // 2.1 填充struct sockaddr_in结构体变量
    bzero(&sin, sizeof(sin));  // 初始化地址结构体
    sin.sin_family = AF_INET;  // 设置地址类型为IPv4
    sin.sin_port = htons(SERV_PORT);  // 设置端口,转换为网络字节序

    // 让服务器程序能绑定任意的IP
#if 1
    sin.sin_addr.s_addr = htonl(INADDR_ANY);  // 绑定到任意地址
#else

    if (inet_pton(AF_INET, SERV_IP_ADDR, (void*)&sin.sin_addr.s_addr) != 1)
    {
        perror("inet_pton");  // 如果IP地址转换失败,打印错误并退出
        exit(1);
    }
#endif
    // 2.2 绑定
    if (bind(fd, (struct sockaddr*)&sin, sizeof(sin)) < 0)
    {
        perror("bind");  // 如果绑定失败,打印错误并退出
        exit(1);
    }

    // 3. 调用listen()把主动套接字变成被动套接字
    if (listen(fd, BACKLOG) < 0)
    {
        perror("listen");  // 如果监听失败,打印错误并退出
        exit(1);
    }

    int newfd = -1;  // 初始化用于接受的文件描述符
    // 4. 阻塞等待客户端连接请求
    // 使用多进程或多线程处理已经建立连接的客户端数据

    pthread_t tid;  // 定义线程ID
    struct sockaddr_in cin;  // 定义客户端地址结构
    socklen_t addrlen = sizeof(cin);  // 定义地址长度
    while (1)
    {
    // 通过程序获取刚建立连接的socket客户端的IP地址和端口号
    
        if ((newfd = accept(fd, (struct sockaddr*)&cin, &addrlen)) < 0)
        {
            
            perror("accept");  // 如果接受连接失败,打印错误并退出
            exit(1);
        }
        char ipv4_addr[16];  // 用于存放IP地址的字符串
        if (!inet_ntop(AF_INET, (void*)&cin.sin_addr, ipv4_addr, sizeof(cin)))
        {
            perror("inet_ntop");  // 如果地址转换失败,打印错误并退出
            exit(1);
        }
        printf("Client(%s:%d) is connected!\n", ipv4_addr, ntohs(cin.sin_port));  // 打印客户端的地址和端口号
        pthread_create(&tid, NULL, (void*)cli_data_handle, (void *)&newfd);  // 创建一个新线程来处理客户端数据

    }
#if 0
    //多进程
    struct sockaddr_in cin;
    socklen_t addrlen = sizeof(cin);
    while(1)
    {
        pid_t pid = -1;  // 用于存放进程ID
        if ((newfd = accept(fd, (struct sockaddr*)&cin, &addrlen)) < 0)
        {
            perror("accept");  // 如果接受连接失败,打印错误并继续监听
            break;  // 退出循环
        }
        // 创建子进程
        if ((pid = fork()) < 0)
        {
            perror("fork");  // 如果创建进程失败,打印错误信息
            break;  // 退出循环
        }
        if (0 == pid)  // 子进程执行的代码
        {
            close(fd);  // 子进程中关闭监听的套接字
            char ipv4_addr[16];  // 用于存放客户端IP地址的字符串
            if (!inet_ntop(AF_INET, (void*)&cin.sin_addr, ipv4_addr, sizeof(cin)))
            {
                perror("inet_ntop");  // 如果IP地址转换失败,打印错误信息
                break;  // 退出循环
            }
            printf("Client(%s:%d) is connected!\n", ipv4_addr, ntohs(cin.sin_port));  // 打印客户端的地址和端口号
            cli_data_handle(&newfd);  // 处理客户端数据
            return 0;  // 子进程完成任务后退出
        }
        else  // 父进程执行的代码
        {
            close(newfd);  // 父进程中关闭连接的套接字,继续监听新的连接
        }
    }
#endif

    close(fd);  // 主进程中关闭监听的套接字
    return 0;  // 主进程退出
}

void cli_data_handle(void* arg)  // 定义处理客户端数据的函数
{
    int newfd = *(int *)arg;  // 从参数中提取客户端的套接字文件描述符
    printf("Child handling process: newfd=%d\n", newfd);  // 打印处理信息
    // 5. 读写
    int ret = -1;  // 用于存放读写操作的返回值
    char buf[BUFSIZ];  // 用于存放读取的数据的缓冲区

    while (1)  // 循环读取客户端发送的数据
    {
        bzero(buf, BUFSIZ);  // 清空缓冲区
        do
        {
            ret = read(newfd, buf, BUFSIZ - 1);  // 从客户端读取数据
        } while (ret < 0 && EINTR == errno);  // 如果读取被信号中断,则重新读取

        if (ret < 0)
        {
            perror("read");  // 如果读取失败,打印错误信息
            exit(1);  // 退出程序
        }
        if (!ret)  // 如果读取到的数据长度为0,表示客户端已关闭
        {
            break;  // 退出循环
        }
        printf("Receive data: %s\n", buf);  // 打印接收到的数据

        if (!strncasecmp(buf, QUIT_STR, strlen(QUIT_STR)))  // 检查是否接收到退出命令
        {
            printf("Client(fd=%d) is exiting!\n", newfd);  // 打印客户端退出信息
            break;  // 退出循环
        }
    }

    close(newfd);  // 关闭与客户端的连接
}

运行效果

 

2,UDP编程

客户端

void usage(char *s) 
{
    printf("\n%s serv_ip serv_port\n",s);  
    printf("\n\t serv_ip:server ip address");  
    printf("\n\t serv_port:server port(>5000)\n\n");  // 提示输入服务器端口,端口号应大于5000
}

int main(int argc,char*argv[])  // 主函数,接收命令行参数
{
    int fd = -1;  // 套接字文件描述符初始化为-1

    int port = -1;  // 端口号初始化为-1
    struct sockaddr_in sin;  // 服务器的地址结构体
    if(argc!=3)  // 检查命令行参数数量是否正确
    {
        usage(argv[0]);  // 如果不正确,显示使用方法
        exit(1);  // 并退出程序
    }
    // 创建socket,使用UDP
    if((fd=socket(AF_INET,SOCK_DGRAM,0))<0)  // SOCK_DGRAM代表使用UDP
    {
        perror("socket");  // 打印socket创建失败的错误信息
        exit(1);  // 退出程序
    }

    port = atoi(argv[2]);  // 将命令行中的端口号字符串转换为整数
    if(port<5000)  // 确保端口号大于5000
    {
        usage(argv[0]);  // 如果不是,则显示使用方法
        exit(1);  // 并退出程序
    }

    // 设置服务器的IP地址和端口号
    bzero(&sin, sizeof(sin));  // 清零地址结构体
    sin.sin_family = AF_INET;  // 指定地址族为IPv4
    sin.sin_port = htons(port);  // 将端口号转换为网络字节序

    // 将IP地址从点分十进制转换为二进制形式
    if(inet_pton(AF_INET,argv[1],(void*)&sin.sin_addr.s_addr)!=1)
    {
        perror("inet_pton");  // 如果转换失败,打印错误信息
        exit(1);  // 退出程序
    }
    // 连接服务器,尽管UDP是无连接的,这里的connect()只设定默认的对方地址
    if(connect(fd,(struct sockaddr*)&sin,sizeof(sin))<0)
    {
        perror("connect");  // 如果连接设置失败,打印错误信息
        exit(1);  // 退出程序
    }

    // UDP客户端启动成功的消息
    printf("UDP client starting......OK!\n");
    char buf[BUFSIZ];  // 数据发送缓冲区
    while(1)  // 循环接收用户输入并发送
    {
        fprintf(stderr, "please input the string to server:");  // 提示用户输入
        bzero(buf, BUFSIZ);  // 清空缓冲区
        if(fgets(buf, BUFSIZ-1, stdin) == NULL)  // 从标准输入读取一行
        {
            perror("fgets");  // 如果读取失败,打印错误信息
            continue;  // 继续下一轮循环
        }
        // 发送数据到服务器
        sendto(fd, buf, strlen(buf), 0, (struct sockaddr*)&sin, sizeof(sin));
        // 检查是否是退出命令
        if(!strncasecmp(buf, QUIT_STR, strlen(QUIT_STR)))
        {
            printf("Client is exiting!\n");  // 如果是,打印退出信息
            break;  // 退出循环
        }
    }
    // 关闭套接字
    close(fd);  // 关闭与服务器的连接
    return 0;  // 程序退出
}

服务器端


#define SERV_PORT 5001  // 服务器监听端口号

#define QUIT_STR "quit"  // 定义退出字符串


int main(void) 
{
    int fd = -1;  // 套接字文件描述符初始化
    struct sockaddr_in sin;  // 服务器地址结构体

    // 创建socket
    if ((fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)  // SOCK_DGRAM代表使用UDP
    {
        perror("socket");  // 如果创建失败,打印错误信息
        exit(1);  // 退出程序
    }

    // 允许绑定地址快速重用
    int b_reuse = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof(int));

    // 绑定socket
    bzero(&sin, sizeof(sin));  // 清零sin结构体
    sin.sin_family = AF_INET;  // 指定地址族为IPv4
    sin.sin_port = htons(SERV_PORT);  // 端口号转换为网络字节序

    // 设置服务器程序能绑定任意的IP
    sin.sin_addr.s_addr = htonl(INADDR_ANY);  // 使用任意地址

    // 绑定
    if (bind(fd, (struct sockaddr*)&sin, sizeof(sin)) < 0)
    {
        perror("bind");  // 如果绑定失败,打印错误信息
        exit(1);  // 退出程序
    }

    char buf[BUFSIZ];  // 数据接收缓冲区
    struct sockaddr_in cin;  // 客户端地址结构体
    socklen_t addrlen = sizeof(cin);  // 客户端地址结构的长度

    printf("UDP server started!\n");  // 通知服务器启动

    while (1)  // 持续接收数据
    {
        bzero(buf, BUFSIZ);  // 清空缓冲区
        // 接收数据
        if (recvfrom(fd, buf, BUFSIZ-1, 0, (struct sockaddr*)&cin, &addrlen) < 0)
        {
            perror("recvfrom");  // 如果接收失败,打印错误信息
            continue;  // 继续接收下一轮数据
        }
        
        char ipv4_addr[16];  // 存储客户端IP地址的字符串
        // 将网络地址转换成“点分十进制”字符串形式
        if (!inet_ntop(AF_INET, (void*)&cin.sin_addr, ipv4_addr, sizeof(ipv4_addr)))
        {
            perror("inet_ntop");  // 如果转换失败,打印错误信息
            exit(1);  // 退出程序
        }
        // 打印接收到的数据和发送者信息
        printf("Received from (%s:%d), data: %s", ipv4_addr, ntohs(cin.sin_port), buf);
       
        // 检查是否接收到退出字符串
        if (!strncasecmp(buf, QUIT_STR, strlen(QUIT_STR)))
        {
            printf("Client (%s:%d) is exiting!\n", ipv4_addr, ntohs(cin.sin_port));  // 打印客户端退出信息
            
        }
    }
    close(fd);  // 关闭套接字
    return 0;  // 程序结束
}

运行效果

 

3,IO多路复用

客户端


void usage(char *s)
{
    printf("\n%s serv_ip serv_port\n", s);  // 显示使用方法
    printf("\n\t serv_ip: server ip address"); // 参数说明
    printf("\n\t serv_port: server port (>5000)\n\n"); // 参数说明
}

int main(int argc, char *argv[])
{
    int fd = -1;  // 套接字文件描述符
    int port = -1;  // 端口号
    struct sockaddr_in sin;  // 服务器的地址结构

    if (argc != 3)  // 检查参数数量
    {
        usage(argv[0]);  // 调用usage函数显示正确的使用方法
        exit(1);  // 退出程序
    }

    // 创建socket
    if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        perror("socket");  // 输出socket创建失败的原因
        exit(1);  // 退出程序
    }

    port = atoi(argv[2]);  // 将端口号字符串转换为整数
    if (port < 5000)  // 检查端口号是否有效
    {
        usage(argv[0]);  // 端口号无效时显示使用方法
        exit(1);  // 退出程序
    }

    // 初始化地址结构
    bzero(&sin, sizeof(sin));  // 清零地址结构
    sin.sin_family = AF_INET;  // 设置地址类型为IPv4
    sin.sin_port = htons(port);  // 端口号转换为网络字节序

    // 将IP地址从点分十进制转换为二进制形式
    if (inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr.s_addr) != 1)
    {
        perror("inet_pton");  // 输出转换错误信息
        exit(1);  // 退出程序
    }

    // 连接服务器
    if (connect(fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
    {
        perror("connect");  // 输出连接失败的原因
        exit(1);  // 退出程序
    }

    int ret = -1;  // 读写操作的返回值
    fd_set rset;  // 文件描述符集合
    int maxfd = -1;  // 最大文件描述符
    struct timeval tout;  // 超时结构体
    char buf[BUFSIZ];  // 数据缓冲区

    while (1)
    {
        FD_ZERO(&rset);  // 清空文件描述符集
        FD_SET(0, &rset);  // 将标准输入加入集合
        FD_SET(fd, &rset);  // 将套接字加入集合
        maxfd = fd;  // 设置最大文件描述符

        tout.tv_sec = 5;  // 设置超时时间为5秒
        tout.tv_usec = 0;  // 设置超时时间微秒部分为0

        select(maxfd + 1, &rset, NULL, NULL, &tout);  // 使用select等待事件

        // 处理标准输入的事件
        if (FD_ISSET(0, &rset))
        {
            bzero(buf, BUFSIZ);  // 清空缓冲区
            do
            {
                ret = read(0, buf, BUFSIZ - 1);  // 从标准输入读取数据
            } while (ret < 0 && errno == EINTR);  // 重试读取如果被信号中断

            if (ret < 0)
            {
                perror("read");  // 输出读取错误信息
                continue;  // 继续下一次循环
            }

            if (!ret)
                continue;  // 如果没有读到数据,继续下一次循环

            // 将读取的数据发送到服务器
            if (write(fd, buf, strlen(buf)) < 0)
            {
                perror("write() to socket");  // 输出写入错误信息
                continue;  // 继续下一次循环
            }

            // 检查是否是退出命令
            if (!strncasecmp(buf, "QUIT", strlen("QUIT")))
            {
                printf("Client is exiting!\n");  // 输出退出信息
                break;  // 退出循环
            }
        }

        // 处理从服务器接收的数据
        if (FD_ISSET(fd, &rset))
        {
            bzero(buf, BUFSIZ);  // 清空缓冲区
            do
            {
                ret = read(fd, buf, BUFSIZ - 1);  // 从套接字读取数据
            } while (ret < 0 && errno == EINTR);  // 重试读取如果被信号中断

            if (ret < 0)
            {
                perror("read from socket");  // 输出读取错误信息
                continue;  // 继续下一次循环
            }

            if (!ret)
                break;  // 如果从服务器读到0字节,表示连接已关闭

            // 输出服务器发送的数据
            printf("server said: %s\n", buf);

            // 检查是否是退出命令
            if (!strncasecmp(buf, "QUIT", strlen("QUIT")))
            {
                printf("Sender Client is exiting!\n");  // 输出退出信息
                break;  // 退出循环
            }
        }
    }

    close(fd);  // 关闭套接字
    return 0;  // 程序正常退出
}

服务器端


#define MAXLINE 4096      // 定义最大行大小
#define SERV_PORT 8080    // 定义服务器端口号

// 定义客户端连接的结构体
typedef struct ClientNode {
    int fd;               // 存储客户端的文件描述符
    struct ClientNode *next; // 指向下一个客户端结构体的指针
} ClientNode;

ClientNode *head = NULL; // 初始化链表的头指针为NULL

// 添加新客户端到链表的函数
void add_client(int fd) {
    ClientNode *newNode = (ClientNode *)malloc(sizeof(ClientNode)); // 分配内存
    newNode->fd = fd;                   // 设置文件描述符
    newNode->next = head;               // 新节点指向原头节点
    head = newNode;                     // 头指针指向新节点
}

// 从链表中删除客户端的函数
void delete_client(int fd) {
    ClientNode *current = head;         // 指向链表的头部
    ClientNode *prev = NULL;            // 前一个节点初始化为NULL
    while (current) {                   // 遍历链表
        if (current->fd == fd) {        // 找到要删除的节点
            if (prev) {                 // 如果不是头节点
                prev->next = current->next; // 前一个节点指向当前节点的下一个节点
            } else {
                head = current->next;   // 如果是头节点,移动头指针
            }
            free(current);              // 释放当前节点内存
            break;
        }
        prev = current;                 // 移动前一个节点
        current = current->next;        // 移动当前节点
    }
}

int main() {
    int listenfd, connfd, maxfd;       // 监听套接字、连接套接字和最大文件描述符
    struct sockaddr_in servaddr;       // 服务器地址结构
    char buf[MAXLINE];                 // 缓冲区
    fd_set allset, rset;               // 文件描述符集合

    listenfd = socket(AF_INET, SOCK_STREAM, 0); // 创建监听套接字
    memset(&servaddr, 0, sizeof(servaddr));     // 初始化地址结构
    servaddr.sin_family = AF_INET;              // 指定地址族为IPv4
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 指定地址为任意地址
    servaddr.sin_port = htons(SERV_PORT);       // 指定端口

    bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); // 绑定地址和端口
    listen(listenfd, 10);                      // 开始监听,最多10个等待连接

    add_client(listenfd); // 将监听套接字添加到客户端链表中

    FD_ZERO(&allset);      // 清空描述符集
    FD_SET(listenfd, &allset); // 将监听套接字加入描述符集
    maxfd = listenfd;      // 初始化最大描述符

    while (1) {
        rset = allset;     // 复制描述符集
        select(maxfd + 1, &rset, NULL, NULL, NULL); // 使用select等待活动的套接字

        for (ClientNode *node = head; node != NULL; node = node->next) { // 遍历客户端链表
            int fd = node->fd;             // 获取当前客户端的文件描述符
            if (FD_ISSET(fd, &rset)) {     // 检查是否有活动
                if (fd == listenfd) {      // 如果是监听套接字
                    // 接受新连接
                    connfd = accept(listenfd, (struct sockaddr *)NULL, NULL);
                    add_client(connfd);     // 添加新客户端到链表
                    FD_SET(connfd, &allset); // 添加新套接字到描述符集
                    if (connfd > maxfd) {
                        maxfd = connfd;    // 更新最大描述符
                    }
                    printf("New client connected: %d\n", connfd);
                } else {
                    // 读取客户端发送的数据
                    memset(buf, 0, MAXLINE);
                    int n = read(fd, buf, MAXLINE);
                    if (n == 0) {
                        // 客户端断开连接
                        close(fd);
                        FD_CLR(fd, &allset);
                        delete_client(fd);
                        printf("Client disconnected: %d\n", fd);
                    } else {
                        printf("Client %d sent: %s", fd, buf);
                        write(fd, buf, n); // 回显数据
                    }
                }
            }
        }
    }

    return 0;
}

运行效果

  • 28
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值