一,通信方式对比分析
信号量主要用于同步多个进程的执行,而消息队列则用于传递消息。共享内存允许多个进程直接访问同一块内存区域,从而实现数据共享。套接字和管道则更多地用于数据传输。
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;
}
运行效果