进程通信-套接字编程(13) 视频教程
IPC
(Inter-Process Conmunication
)进程间通信
一、管道 管道必须凑齐读写双方才可以实现
管道可以看作为队列。先入先出,队首出,队尾入
在C语言中,使用管道(pipes)进行进程间通信是一种经典方法。管道主要用于进程间传递数据
管道由内核提供,单工,具有自同步机制
自同步机制 :管道的自同步机制指的是管道在进程间通信时内置的同步特性,这些特性确保了数据的一致性和顺序性。管道通常是阻塞的,这意味着在特定情况下,读操作和写操作会被阻塞 ,直到某些条件得到满足。这种行为形成了管道的基本自同步特性。
管道分为:匿名管道与命名管道
1.匿名管道 匿名管道是一种简单的进程间通信(IPC
)机制,主要用于有父子关系的进程间的通信。在Unix和类Unix系统(如Linux)中,匿名管道非常常见。它们是单向的通信方式,通常用于一个进程向另一个进程(具有父子关系的进程)发送数据
基本特性
单向通信 :数据只能在一个方向上流动,要么是从父进程到子进程,要么是从子进程到父进程。
临时通信通道 :管道在使用完毕后会被销毁。
数据流 :管道中的数据是按照先进先出(FIFO
)的顺序流动的。
内存中的存在 :匿名管道不对应于文件系统中的任何文件,它们仅存在于内存中。
创建与使用
在C语言中,可以通过pipe()
系统调用来创建一个匿名管道。这个调用会创建管道并返回两个文件描述符:一个用于读(pipefd[0]
),另一个用于写(pipefd[1]
)。
(1)pipe函数 在Unix和类Unix系统中,pipe()
函数用于创建一个匿名管道,用于在进程间进行通信。这个函数是进程间通信(IPC
)的基础工具之一,特别是在需要在有父子关系的进程之间传递数据时。
函数原型
在 C 语言中,pipe()
函数的原型定义在 <unistd.h>
头文件中,如下所示:
1 2 3 #include <unistd.h> int pipe (int pipefd[2 ]) ;
参数
pipefd
:一个包含两个整型元素的数组。函数成功执行后,pipefd[0]
会被设置为管道的读端,而 pipefd[1]
会被设置为管道的写端
返回值
成功时,返回 0
失败时,返回 -1
并设置 errno
以指示错误原因
使用方式
当你调用 pipe()
函数时,它会创建一个管道并提供两个文件描述符(0端作为读取端口,1端作为写入端口):一个用于读取管道(pipefd[0]
),另一个用于写入管道(pipefd[1]
)。这些文件描述符可以在后续的读写操作中使用
实例程序
main.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #define BUFSIZE 1024 int main () { pid_t pid; char buf[BUFSIZE]; int len; int pd[2 ]; if (pipe(pd)<0 ) { perror("pipe()" ); exit (1 ); } pid = fork(); if (pid < 0 ) { perror("fork()" ); exit (1 ); } if (pid == 0 ) { close(pd[1 ]); len = read(pd[0 ],buf,BUFSIZE); write(1 ,buf,len); close(pd[0 ]); exit (0 ); }else { close(pd[0 ]); write(pd[1 ],"HELLO!\n" ,7 ); close(pd[1 ]); wait(NULL ); } exit (0 ); }
运行结果
2.命名管道 命名管道(也称为 FIFO
,即“先进先出”)是一种在不相关的进程之间进行通信的机制。与匿名管道不同,命名管道具有在文件系统中实际存在的路径名,这允许不具有共同祖先的进程之间进行通信
基本特性
有名字 :与匿名管道只存在于内存中不同,命名管道在文件系统中有一个实际的名字。
持久性 :即使没有进程使用它们,命名管道也会继续存在。
双向通信 :理论上支持双向通信,但在实际使用中通常作为单向通信使用,以避免复杂的同步问题。
先进先出原则 :数据以先进先出的方式传输。
使用场景
两个不相关的进程通信 :例如,一个进程负责生成数据,另一个进程负责处理数据。
跨程序通信 :不同程序之间可以通过命名管道进行简单的数据交换。
注意事项
命名管道的读取和写入操作通常是阻塞的,除非特别设置为非阻塞方式
需要确保合适地打开和关闭管道,避免资源泄露
与匿名管道类似,命名管道的数据也是没有内部结构的字节流,因此需要额外的协议或约定来解释数据。
创建与使用
在Unix和类Unix系统中,命名管道通常可以通过两种方式创建:
mkfifo
是 Unix 和类 Unix 系统(如 Linux)中用于创建命名管道(FIFO)的函数命名管道允许不相关的进程进行通信,不同于匿名管道,它在文件系统中以特定名称存在,而不仅仅是进程间的临时通信通道
1 2 3 4 #include <sys/types.h> #include <sys/stat.h> int mkfifo (const char *pathname, mode_t mode) ;
(1)命令行创建管道
查看创建的管道信息
如下,文件权限被标明为p
,表明这是一个管道文件
可以使用date
显示时间日期信息
在一个终端,将data信息重定向,输入至管道(内容写入管道),如下
另外打开一个终端,读取管道内的内容
二、消息队列 1.ftok函数 在 Unix 和类 Unix 系统中,ftok
是一个用于 IPC
(进程间通信)的标准库函数,它根据一个现有的文件名和一个字节大小的项目标识符,生成一个 System V IPC
键(通常称为 key_t
类型)。这个键通常用于创建或访问消息队列、共享内存或信号量
功能
ftok
函数的主要目的是提供一种方法,以便不同的进程可以使用相同的键来访问同一段共享内存、消息队列或信号量。通过这种方式,可以保证在不同的进程中创建或访问相同的IPC
结构
ftok
函数是可以获得一个用于进程间通信IPC
的key_t
值
函数原型
1 2 3 #include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id);
pathname
:是指向一个已存在文件的路径的指针。这个文件用作生成键的基础。
proj_id
:是一个字符,用于和 pathname
一起生成一个独特的键。它通常是一个非零字符。
返回值
成功时,ftok
返回一个 key_t
类型的值,该值可以被用于后续的IPC
操作。
失败时,返回 (key_t) -1
,并设置 errno
以指示错误原因。
注意事项
ftok
并不保证生成的键是唯一的,尽管在大多数情况下它是足够有效的。
使用 ftok
时,确保参考文件存在且对所有需要通信的进程可访问。
由于 ftok
返回的键可能在不同系统上有所不同,因此它不适合用于跨系统的 IPC
2.消息队列的操作 消息队列是一个双工的操作
(1)msgget函数 在 Unix 和类 Unix 系统中,msgget
函数是用于创建或访问消息队列的系统调用。消息队列是一种进程间通信(IPC
)机制,允许进程以消息的形式交换数据。这些消息按照先进先出(FIFO
)的顺序在队列中排列
功能
msgget
函数用于创建新的消息队列或获取对现有消息队列的访问权限。它是 System V IPC
机制的一部分
函数原型
1 2 3 4 5 #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgget (key_t key, int msgflg) ;
key
:是一个IPC
键,通常由 ftok
函数生成。特殊值 IPC_PRIVATE
可用于创建一个新的、私有的消息队列。
msgflg
是一个整数,指定操作模式和权限。它可以是权限位的组合(类似于文件权限,例如 0644),以及可能包括以下标志之一:
IPC_CREAT
:如果指定的键不存在,则创建一个新的消息队列。
IPC_EXCL
:与 IPC_CREAT
结合使用时,如果消息队列已存在,则返回错误
返回值
成功时,msgget
返回一个非负整数,表示消息队列的标识符(ID)。
失败时,返回 -1,并设置 errno
以指示错误原因
注意事项
确保在使用 msgget
之前调用 ftok
生成有效的 IPC
键
使用 msgget
创建的消息队列在系统级别是持久的,除非显式删除(使用 msgctl
)或系统重启
使用消息队列进行进程间通信时,需要定义一种消息结构 ,并遵循先进先出的原则进行操作
(2)msgsnd与msgrcv函数 msgsnd msgsnd
用于向消息队列发送消息
函数原型
1 2 3 4 5 #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgsnd (int msqid, const void *msgp, size_t msgsz, int msgflg) ;
msqid
:消息队列标识符,由 msgget
函数返回。
msgp
:指向消息结构体的指针。消息结构体的第一个字段通常是一个长整型 (long
),用于表示消息类型。
msgsz
:消息数据部分的长度,不包括消息类型字段。
msgflg
:控制消息发送行为的标志。常用的标志包括:
IPC_NOWAIT
:如果消息不能立即发送(例如,队列已满),则不等待,直接返回。
如果不设置 IPC_NOWAIT
,则在无法立即发送消息时,调用将阻塞直到能够发送为止。
返回值
成功时返回 0。
失败时返回 -1,并设置 errno
。
msgrcv msgrcv
用于从消息队列接收消息
函数原型
1 2 3 4 5 #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> ssize_t msgrcv (int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg) ;
msqid
:消息队列标识符,由 msgget
函数返回。
msgp
:指向消息结构体的指针。消息结构体的第一个字段通常是一个长整型(long
),用于表示消息类型。
msgsz
:消息数据部分的大小,不包括消息类型字段。
msgtyp
:指定接收消息的类型。如果 msgtyp
为 0,则接收队列中的第一个消息;如果 msgtyp
大于 0,则接收队列中第一个类型等于 msgtyp
的消息;如果 msgtyp
小于 0,则接收队列中类型值最小且不大于 msgtyp
绝对值的第一个消息。
msgflg
:控制消息接收行为的标志。常用的标志包括:
IPC_NOWAIT
:如果没有符合条件的消息,则不等待,直接返回。
MSG_NOERROR
:如果队列中的消息长度超过 msgsz
,则截断消息而不是报错。
如果不设置 IPC_NOWAIT
,则在没有符合条件的消息时,调用将阻塞直到有消息为止。
返回值
成功时返回接收到的消息的大小(字节)。
失败时返回 -1,并设置 errno
以指示错误的原因。
3.msgctl msgctl
函数是 Unix 和类 Unix 系统中的一个系统调用,用于控制消息队列 。它是 System V
消息队列接口的一部分,提供了对消息队列执行各种控制操作的功能 ,如改变队列属性、获取队列信息和删除队列
函数原型
1 2 3 4 5 #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgctl (int msqid, int cmd, struct msqid_ds *buf) ;
msqid
:消息队列标识符,由 msgget
函数返回。
cmd
:指定要执行的控制操作。常用的操作包括:
IPC_STAT
:获取消息队列的当前状态和属性,并将其存储在 buf
指向的 msqid_ds
结构体中。
IPC_SET
:根据 buf
指向的 msqid_ds
结构体中的值设置消息队列的属性。
IPC_RMID
:立即删除消息队列,释放与之关联的所有资源。
buf
:指向 msqid_ds
结构体的指针,用于存储或设置消息队列的状态信息。对于 IPC_STAT
和 IPC_SET
命令,这个参数是必需的。对于 IPC_RMID
命令,它通常被忽略。
返回值
成功时返回 0。
失败时返回 -1,并设置 errno
以指示错误的原因。
4.实例代码 创建一个发送端,向消息队列中发送信息,再创建一个接收端,取出消息队列中的信息
准备工作 :在项目目录下创建文件,命名为Communication_Service
的文件,将该文件的路径传入ftok
函数中用于生成进程通信的IPC
键
(1)协议 创建通信协议,定义收发数据的结构体
proto.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #ifndef MSG_PROTO_H #define MSG_PROTO_H #include <sys/ipc.h> #include <sys/types.h> #include <sys/msg.h> #define KEYPATH "./Communication_Service" #define KEYPROJ 'g' #define NAMESIZE 1024 struct msg_st { long mtype; char name[NAMESIZE]; int math; int chinese; }; #endif
(2)接收端 接收消息队列中的数据信息,在该实例程序中,应该先运行接收端程序,等待消息队列中有数据时,取出
rcver.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 #include <stdlib.h> #include <stdio.h> #include "proto.h" int main () { key_t key; int msgid; struct msg_st rbuf ; key = ftok(KEYPATH,KEYPROJ); if (key < 0 ) { perror("ftok()" ); exit (1 ); } msgid = msgget(key,IPC_CREAT|0600 ); if (msgid < 0 ) { perror("msgget()" ); exit (1 ); } while (1 ) { if (msgrcv(msgid,&rbuf,(sizeof (rbuf)-sizeof (long )),0 ,0 ) < 0 ) { perror("msgrcv()" ); exit (1 ); } printf ("NAME = %s\n" ,rbuf.name); printf ("MATH = %d\n" ,rbuf.math); printf ("CHINESE = %d\n" ,rbuf.chinese); } if (msgctl(msgid,IPC_RMID,NULL ) < 0 ) { perror("msgctl()" ); exit (1 ); } exit (0 ); }
(3)发送端 snder.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #include <stdio.h> #include <stdlib.h> #include <string.h> #include "proto.h" int main () { key_t key; struct msg_st sbuf ; int msgid; key = ftok(KEYPATH,KEYPROJ); if (key < 0 ) { perror("ftok()" ); exit (1 ); } msgid = msgget(key,0 ); if (msgid < 0 ) { perror("msgget()" ); exit (1 ); } sbuf.mtype = 1 ; strcpy (sbuf.name,"ZXZ" ); sbuf.math = 100 ; sbuf.chinese = 100 ; if (msgsnd(msgid,&sbuf,(sizeof (sbuf)-sizeof (long )),0 ) < 0 ) { perror("msgsnd()" ); exit (1 ); } puts ("Send OK!\n" ); exit (0 ); }
(4)运行结果 在项目目录下,打开两个终端,分别对收发端程序源码进行编译。如下:
运行接收端,可执行文件,等到发送端,发送信息
运行发送端发送信息
发送端终端结果:
接收端终端结果:
结束程序之后,在项目目录下的终端运行命令
可以发现我们创建的消息队列实例没有被删除
可以使用如下命令对消息队列,或者共享内存或者信号量数组进行删除
使用方法如下:
ipcrm
是 Unix 和类 Unix 系统中用于删除 IPC(
进程间通信)资源的命令行工具。这个工具允许用户删除消息队列、共享内存段和信号量集,这些是 System V IPC
的主要构成部分
ipcrm
的基本语法如下:
常见的选项包括:
-q msqid
:删除消息队列 ID 为 msqid
的消息队列。
-m shmid
:删除共享内存段 ID 为 shmid
的共享内存。
-s semid
:删除信号量集 ID 为 semid
的信号量集。
-Q msgkey
:删除键为 msgkey
的消息队列。
-M shmkey
:删除键为 shmkey
的共享内存段。
-S semkey
:删除键为 semkey
的信号量
针对我们实例程序中的消息队列可以使用,msqid进行删除
运行上述命令之后再进行查看,创建的消息队列被删除:
5.消息队列-ftp实例 使用消息队列在接收端与发送端之间进行数据的收发
实例描述:
Client端,向Server端发送path请求,path表示的是文件路径
Server端,接收Client的path请求,根据path请求,找到path路径下对应的数据
Server端,将path下的数据分为多份data包发送给Client端,若数据发送完毕,则发送EOF(End of transmission)
数据包告知Client发送完毕
图解如下:
(1)收发协议 两种方式实现,Client端不同数据包的判断
proto.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #ifndef MYFTP_MSG_PROTO_H #define MYFTP_MSG_PROTO_H #define KEYPATH "./Communication_Service" #define KEYPROJ 'g' #define PATHMAX 1024 #define DATAMAX 1024 enum { MSG_PATH = 1 , MSG_DATA, MSG_EOT }; typedef struct msg_path_st { long mtype; char path[PATHMAX]; }msg_path_t ; typedef struct msg_data_st { long mtype; char data[DATAMAX]; int datalen; }msg_data_t ; typedef struct msg_eot_st { long mtype; }msg_eot_t ; union msg_s2c_un { long mtype; msg_data_t datamsg; msg_eot_t etomsg; }; #endif
proto2.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #ifndef MYFTP_MSG_PROTO2_H #define MYFTP_MSG_PROTO2_H #define KEYPATH "./Communication_Service" #define KEYPROJ 'g' #define PATHMAX 1024 #define DATAMAX 1024 enum { MSG_PATH = 1 , MSG_DATA, MSG_EOT }; typedef struct msg_path_st { long mtype; char path[PATHMAX]; }msg_path_t ; typedef struct msg_data_st { long mtype; int datalen; char data[DATAMAX]; }msg_data_t ; #endif
6.消息队列-信号量数组实例 信号量数组是一种在并发编程中使用的同步机制,它通常用于控制对共享资源的访问。在Unix和类Unix系统中,信号量通常是通过系统调用和IPC
(进程间通信)机制实现的。信号量数组就是包含多个信号量的集合,允许程序同时管理多个共享资源。
若信号量数组,的大小为1
则表示信号量或者互斥锁,对于单个资源的单独共享
基本概念
信号量(Semaphore) :一个整数值,用于同步进程或线程,控制对共享资源的访问。
信号量数组 :包含多个信号量的集合,每个信号量可以独立地用来控制对不同共享资源的访问。
主要操作
初始化 :在使用信号量之前,需要初始化它们,包括设置初始值。
等待(P操作或wait) :如果信号量的值大于0,减少它的值并继续执行。如果信号量的值为0,进程或线程将阻塞,直到信号量值变为正。
发信号(V操作或signal) :增加信号量的值。如果有其他进程或线程因信号量的值为0而阻塞,它们中的一个将被唤醒
使用场景
信号量数组在多进程环境中特别有用,特别是当你需要控制对多个不同资源的并发访问时 。例如,你可能有一个包含多个文件或数据结构的系统,每个文件或数据结构都需要单独的同步机制
(1)信号量集合相关API semget semget
函数是 UNIX 和类 UNIX 系统(如 Linux)中用于创建新的信号量集或访问现有信号量集的系统调用 。它是实现进程间同步和互斥的关键组件之一
函数原型
1 2 3 4 5 #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semget (key_t key, int nsems, int semflg) ;
参数说明
key :一个键值(key_t
类型),用于标识信号量集。可以是一个显式的键值或使用 IPC_PRIVATE
创建一个新的私有集合。
nsems :需要的信号量数目。如果访问现有的信号量集,这个值通常设置为 0。
semflg :一个整数,指定一组标志。这些标志包括创建信号量集的权限(如 0600
或 0666
等)和其他选项,如 IPC_CREAT
(创建新信号量集,如果已存在则访问它)和 IPC_EXCL
(与 IPC_CREAT
结合使用,确保创建新的集合)
返回值
成功时,返回信号量集的标识符(非负整数)。
出错时,返回 -1,并设置 errno
以指示错误的原因(例如 EEXIST
表示信号量集已存在,ENOENT
表示不存在等)。
semop semop
函数是 UNIX 和类 UNIX 系统(比如 Linux)中用于执行操作(如等待和信号)在信号量上的系统调用 。这个函数允许进程对一个或多个信号量执行一系列操作,这对于进程间同步和互斥是非常重要的
函数原型
1 2 3 4 5 #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semop (int semid, struct sembuf *sops, size_t nsops) ;
参数说明
semid :由 semget
返回的信号量集标识符。
sops :指向 sembuf
结构数组的指针,每个元素指定对单个信号量执行的操作。
nsops :sops
数组中的元素数量,表示要执行的操作数。
sembuf
结构定义如下:
1 2 3 4 5 struct sembuf { unsigned short sem_num; short sem_op; short sem_flg; };
返回值
成功时,返回 0。
出错时,返回 -1,并设置 errno
以指示错误的原因(例如 EINTR
表示操作被中断,EINVAL
表示参数无效等)。
semctl semctl
函数是 UNIX 和类 UNIX 系统(如 Linux)中用于控制信号量集的系统调用 。它提供了一种方式来执行多种不同的控制操作,比如设置信号量的值,获取信号量的当前值,或删除信号量集
函数原型
1 2 3 4 5 #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semctl (int semid, int semnum, int cmd, ...) ;
参数说明
semid :由 semget
返回的信号量集标识符。
semnum :信号量集中的信号量索引(对于某些命令,这个参数可能被忽略)。
cmd :控制操作的类型,如 SETVAL
(设置信号量的值),GETVAL
(获取信号量的当前值),IPC_RMID
(删除信号量集)等。
**…**:附加参数,根据 cmd
的不同而变化。例如,对于 SETVAL
,它是一个 union semun
类型的值,用于设置信号量的值。
共用体uniom semum
定义 由于历史原因,union semun
并不总是在系统头文件中定义。因此,你可能需要自己定义它,如下所示
1 2 3 4 5 union semun { int val; struct semid_ds *buf ; unsigned short *array ; };
返回值
成功时,根据执行的命令不同,返回值也不同。例如,GETVAL
返回信号量的当前值。
出错时,返回 -1,并设置 errno
以指示错误的原因(例如 EINVAL
表示参数无效,EACCES
表示权限错误等)。
(2)实例程序 使用信号量数组,实现互斥锁,保证对于单个进程共享资源的单独使用,进程同步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <errno.h> #define PROCNUM 20 #define FNAME "../out" #define LINESIZE 1024 static int semid;static void P (void ) { struct sembuf op ; op.sem_num = 0 ; op.sem_op = -1 ; op.sem_flg = 0 ; while (semop(semid,&op,1 )<0 ) { if (errno != EINTR || errno != EAGAIN) { perror("semop()" ); exit (1 ); } } } static void V (void ) { struct sembuf op ; op.sem_num = 0 ; op.sem_op = 1 ; op.sem_flg = 0 ; if (semop(semid,&op,1 )<0 ) { perror("semop()" ); exit (1 ); } } static void func_add (void ) { FILE *fp; char linebuf[LINESIZE]; fp = fopen(FNAME,"r+" ); if (fp ==NULL ) { perror("fopen()" ); exit (1 ); } P(); fgets(linebuf,LINESIZE,fp); fseek(fp,0 ,SEEK_SET); sleep(1 ); fprintf (fp,"%d\n" ,atoi(linebuf)+1 ); fflush(fp); V(); fclose(fp); return ; } int main () { int i; pid_t pid; semid = semget(IPC_PRIVATE,1 ,IPC_CREAT|0600 ); if (semid < 0 ) { perror("semget()" ); exit (1 ); } if (semctl(semid,0 ,SETVAL,1 ) < 0 ) { perror("semctl()" ); exit (1 ); } for (i = 0 ;i<PROCNUM;i++) { pid = fork(); if (pid < 0 ) { perror("fork()" ); exit (1 ); } if (pid == 0 ) { func_add(); exit (0 ); } } for (i = 0 ;i<PROCNUM;i++) { wait(NULL ); } semctl(semid,0 ,IPC_RMID); exit (0 ); }
上述代码,可以实现多进程对out文件逐次递加使用
7.进程间通信-共享内存项目实例 共享内存是一种进程间通信(IPC)机制,允许两个或多个进程共享一个给定的存储区。它是最快的IPC方法之一,因为进程是直接对内存进行读写,而不是通过操作系统进行数据传输
如何工作
在共享内存模型中,操作系统为多个进程分配一块内存区域。一旦这块内存被分配,所有有访问权限的进程都可以直接读写这块内存。这些进程可能是同时运行的,并且可以看到内存中的即时更新。
共享内存优点
效率高 :由于数据不需要在进程之间复制,所以这是一种非常快速的数据交换方式。
实时通信 :共享内存允许进程以几乎无延迟的方式进行通信。
共享内存优点
(1)共享内存相关API shmget shmget
是 UNIX 和类 UNIX 系统(比如 Linux)中用于创建新的共享内存段或获取现有共享内存段的标识符的系统调用 。共享内存是一种高效的进程间通信(IPC)机制,允许多个进程访问同一块内存区域
1 2 3 4 5 #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> int shmget (key_t key, size_t size, int shmflg) ;
参数说明
key :一个键值(key_t
类型),用于标识共享内存段。可以是一个显式的键值或使用 IPC_PRIVATE
创建一个新的私有内存段。
size :需要的共享内存大小(以字节为单位)。
shmflg :一个整数,指定一组标志。这些标志包括创建共享内存段的权限(如 0600
或 0666
等)和其他选项,如 IPC_CREAT
(创建新共享内存段,如果已存在则访问它)和 IPC_EXCL
(与 IPC_CREAT
结合使用,确保创建新的段)
返回值
成功时,返回共享内存段的标识符(非负整数)。
出错时,返回 -1,并设置 errno
以指示错误的原因(例如 EEXIST
表示共享内存段已存在,ENOENT
表示不存在等)
shmat shmat
(shared memory attach)函数是 UNIX 和类 UNIX 系统中用于将共享内存段附加到进程的地址空间的系统调用。这个函数使进程能够访问共享内存段中的数据 。
函数原型
1 2 3 4 #include <sys/types.h> #include <sys/shm.h> void *shmat (int shmid, const void *shmaddr, int shmflg) ;
参数说明
shmid :由 shmget
返回的共享内存段标识符。
shmaddr :指定共享内存连接到进程地址空间的特定地址。如果为 NULL,则操作系统选择地址。
shmflg :附加标志。常用的标志包括 SHM_RND
(用于舍入 shmaddr
到系统允许的地址边界)和 SHM_RDONLY
(将内存段附加为只读)
返回值
成功时,返回指向共享内存段的指针。
出错时,返回 ((void *) -1)
,并设置 errno
以指示错误的原因。
shmdt shmdt
函数是 UNIX 和类 UNIX 系统(如 Linux)中用于将共享内存段从进程的地址空间分离的系统调用。当进程完成对共享内存的访问并希望解除与该内存段的关联时,它会使用此函数 。
函数原型
1 2 3 4 #include <sys/types.h> #include <sys/shm.h> int shmdt (const void *shmaddr) ;
参数说明
shmaddr :指向共享内存段的指针,这个指针是之前调用 shmat
时返回的
返回值
成功时,返回 0
。
出错时,返回 -1
,并设置 errno
以指示错误的原因。
shmctl shmctl
是 UNIX 和类 UNIX 系统(如 Linux)中用于执行共享内存控制操作的系统调用。这个函数允许你对共享内存进行多种操作,如获取状态信息、修改权限、标记删除等
函数原型
1 2 3 4 5 #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> int shmctl (int shmid, int cmd, struct shmid_ds *buf) ;
参数说明
shmid :共享内存段的标识符,通常由 shmget
返回。
cmd :控制命令,决定了 shmctl
的行为。常用命令包括 IPC_STAT
(获取共享内存状态)、IPC_SET
(设置共享内存参数)和 IPC_RMID
(标记共享内存以便删除)。
buf :指向 shmid_ds
结构的指针,用于获取或设置共享内存的状态信息。结构定义如下:
1 2 3 4 5 6 7 8 9 10 struct shmid_ds { struct ipc_perm shm_perm ; size_t shm_segsz; time_t shm_atime; time_t shm_dtime; time_t shm_ctime; pid_t shm_cpid; pid_t shm_lpid; shmatt_t shm_nattch; };
返回值
成功时,返回 0
。
出错时,返回 -1
,并设置 errno
以指示错误的原因。
注意
使用 IPC_RMID
命令标记共享内存以便删除后,该内存段仍然存在,直到所有附加的进程都分离(即 shm_nattch
为0)。
在共享内存段被删除之前,它仍然可以被其他进程附加和使用。
适当地管理共享内存非常重要,特别是在多进程系统中,以防止资源泄露。
(2)实例程序 使用共享内存用于父子进程间的通信
描述:子进程向共享内存中写入数据HELLO
,父进程读取共享内存中的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <unistd.h> #include <string.h> #include <wait.h> #define MEMSIZE 1024 int main () { int shmid; pid_t pid; char *ptr; shmid = shmget(IPC_PRIVATE,MEMSIZE,IPC_CREAT|0600 ); if (shmid < 0 ) { perror("shmget()" ); exit (1 ); } pid = fork(); if (pid < 0 ) { perror("fork()" ); exit (1 ); } if ( pid == 0 ) { ptr = shmat(shmid,NULL ,0 ); if (ptr == (void *)-1 ) { perror("shmat()" ); exit (1 ); } strcpy (ptr,"HELLO!\n" ); shmdt(ptr); exit (0 ); }else { wait(NULL ); ptr = shmat(shmid,NULL ,0 ); if (ptr == (void *)-1 ) { perror("shmat()" ); exit (1 ); } puts (ptr); shmdt(ptr); shmctl(shmid,IPC_RMID,NULL ); exit (0 ); } }
运行结果
三、套接字 1.跨主机传输要注意的主要问题 (1)基础知识 大小端存储 大小端存储(Big-endian
和 Little-endian
)是计算机系统中用于存储数据字节序的两种不同方式 。它们主要涉及多字节数据(如整数、浮点数)在内存中的存储顺序
大端模式可视作更接近人类阅读和书写数字的方式
小端模式 :
在小端模式中,最低有效字节(LSB
)存储在最低的内存地址上,而最高有效字节(MSB
)存储在最高的内存地址上(低地址放低字节 )
同样的例子,值 0x12345678
的四字节整数,在内存中的存储顺序将是 78 56 34 12
小端模式在许多现代计算机系统中(如x86
架构)更为常见
注意(重点理解) :
在 0x12345678
中,0x12
始终是最高有效字节,而 0x78
始终是最低有效字节(LSB
)
在十六进制(16进制)系统中,每位可以表示16个不同的值,范围从0到15。然而,为了在数字表示中包含超过9的值,十六进制使用字母A到F来代表10到15。因此,十六进制的每位可以是0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, 或 F
所以,当你看到一个像 0x12345678
这样的十六进制数时,每个数字或字母都代表一个单独的十六进制位。这个数分解如下:
1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
每个都是单独的十六进制位
12
, 34
, 56
, 78
每对数字代表一个字节,因为在十六进制中,每两位组成一个8位(或1字节)的值(在十六进制(16进制)中,每两位组成一个8位(或1字节)的值,这是因为每个十六进制位可以表示4个二进制位,而两个16进制位则是8个二进制位,即一个字节)
32位机器与64位机器数据类型存储字节数
int
类型
在32位的机器环境中,int
类型的数据通常占用4个字节。这是因为在大多数32位系统中,标准的整型(int
)被定义为32位,而每个字节包含8位,因此 32位 / 8位/字节 = 4字节
在64位的机器环境中,int
类型数据的大小并不总是因为系统是64位的就自动扩展到64位。实际上,int
类型的大小取决于编译器和操作系统的具体实现,但在大多数现代的64位系统中,int
仍然保持为32位,即4个字节。这是为了保持与旧代码的兼容性。不过,其他一些数据类型,如指针(pointers
)和长整型(long
),在64位系统中可能会增加到64位
(2)字节序问题 跨主机通信时字节序问题是计算机网络中的一个重要考虑因素。不同的计算机系统可能采用不同的字节序(即大小端模式)来存储数据 ,这可能导致在一个系统中正确表示的数据,在另一个系统中被错误解释。
机器对于文件数据按地址位读取方式 :数据通常是按照它们在文件中的顺序逐字节读取的。这意味着读取操作通常是从文件的开始(低地址位)向文件的结束(高地址位)进行的
若在跨主机通信中,收发两端采用不同的字节序来存储数据,则解析数据时会存在问题 。
问题根源:
不同的字节序 :有些系统采用大端模式(如大多数网络设备和一些旧的计算机系统),而其他系统则采用小端模式(如大多数现代个人电脑和服务器)。
直接传输未转换的数据 :如果一个系统直接将其内部表示的数据发送到另一个采用不同字节序的系统,接收方可能会错误地解释这些数据。
解决方法 :
网络字节序 :为了解决这个问题,网络通信通常使用一种称为“网络字节序”的标准,它是大端模式。因此,在数据在网络上发送之前,它通常被转换为网络字节序。接收系统需要将接收到的数据从网络字节序转换回其本地(主机)字节序
明确协议规范 :通信协议应该明确指定所使用的字节序;在设计协议时,应该考虑到不同系统间的字节序差异,并在必要时进行适当的转换
使用无关字节序的格式 :另一种方法是使用格式(如JSON
、XML
)来交换数据,这些格式本质上是字节序无关的,因为它们表示的是文本数据
转换函数 :在需要时,开发者可以使用如 htonl()
、htons()
(host-to-network long/short)和它们的反向函数 ntohl()
、ntohs()
(network-to-host long/short
)等函数来在本地字节序和网络字节序之间进行转换
网络字节序 网络字节序是一种在网络上发送和接收数据时使用的字节序约定。由于不同的计算机系统可能使用不同的字节序(即大端模式和小端模式),因此在通过网络通信时,需要一个共同的标准以确保数据的一致性和正确性。网络字节序就是这样一个标准,它采用大端模式
网络字节序特点 :
使用大端模式
跨平台一致性 :采用网络字节序可以确保来自不同字节序计算机系统的数据在网络上传输时保持一致的表示方式
应用 :
协议遵守 :许多网络协议,如TCP/IP
,明确规定使用网络字节序来表示多字节数值。因此,构建和解析网络数据包时需要遵循这一约定
数据转换 :在发送数据之前,发送方可能需要将其本地字节序的数据转换为网络字节序。接收方收到数据后,可能需要将网络字节序的数据转换回其本地字节序。这通常通过函数(如 htonl()
、htons()
、ntohl()
、ntohs()
等)完成,这些函数在不同平台上有不同的实现,但提供一致的接口。这些函数中h
表示host
主机的意思,to
表示从前者到后者的意思,而n
表示network
,最后的s与l
则表示short
与long
类型的数据
(3)结构体对齐问题 在数据存储中,为了加速地址检索,结构体会存在地址对齐,但是在网络通信的结构体中需禁止对齐(指定告知编译器不需要进行地址对齐)
结构体对齐方式
(4)数据类型长度问题 例如:不同机器之间int
占多少字节,占多少二进制位。char
字符数据,有无符号
解决方法 :
使用通用的类型数据:如32位的整型数,int32_t
;32位无符号整型数据,uint32_t
;类推,int64_t
,int8_t
,uint8_t
2.套接字-Socket Socket(套接字)是一种网络通信的端点,它为网络中的不同主机之间提供了一种发送和接收数据的方式。Socket是计算机网络通信的基础,存在于许多不同类型的网络协议中,最常见的是基于TCP/IP协议的Socket。在应用层和网络层之间,套接字充当了一个接口的角色
套接字关键特点
通信端点 :Socket是网络通信的端点,每个Socket都有一个对应的IP地址和端口号
支持不同的通信类型 :套接字可以用于不同类型的网络通信,如TCP(传输控制协议)和UDP(用户数据报协议)
面向连接和无连接 :TCP套接字是面向连接的,意味着在数据传输之前需要建立一个稳定的连接;UDP套接字是无连接的,它们发送独立的数据包而不需要建立稳定的连接
数据传输 :套接字允许应用程序通过网络发送和接收数据
(1)套接字相关API socket() socket()
函数用于创建一个新的套接字,它是网络通信的基本操作之一。这个函数定义了套接字的类型、所使用的协议族以及具体的协议类型。它的原型如下:
1 2 3 4 #include <sys/types.h> #include <sys/socket.h> int socket (int domain, int type, int protocol) ;
参数说明 :
domain
:指定套接字使用的协议族,常见的有 AF_INET
(表示IPv4网络协议)、AF_INET6
(表示IPv6网络协议)等
type
:指定套接字的类型。常见的类型有 SOCK_STREAM
(表示面向连接的套接字,通常用于TCP)和 SOCK_DGRAM
(表示无连接的套接字,通常用于UDP)
protocol
:指定具体的协议。通常设置为0,表示选择默认协议(对于 SOCK_STREAM
是TCP,对于 SOCK_DGRAM
是UDP)
返回值 :
成功时,返回一个套接字描述符(一个正整数)。
失败时,返回-1,并设置 errno
以指示错误原因
bind() bind()
函数用于将套接字与特定的IP地址和端口号绑定 。它通常用于服务器端的套接字,以便指定和监听来自客户端的连接请求
函数原型 :
1 2 3 4 #include <sys/types.h> #include <sys/socket.h> int bind (int sockfd, const struct sockaddr *addr, socklen_t addrlen) ;
sockfd
:是由 socket()
函数调用返回的套接字文件描述符。
addr
:是指向 struct sockaddr
结构的指针,该结构包含了要绑定的IP地址和端口号。
addrlen
:指定了 addr
结构的大小。
返回值 :
成功 :当 bind()
函数成功绑定套接字到指定的地址和端口时,它返回 0
。
失败 :如果出现错误,bind()
函数返回 -1
。此外,全局变量 errno
被设置为一个特定的错误代码,以提供有关错误的更多信息
使用步骤 :
创建一个套接字使用 socket()
。
填充 struct sockaddr
结构(或其它相关结构,如 struct sockaddr_in
对于IPv4)。
调用 bind()
将套接字与指定的IP地址和端口号绑定。
struct sockaddr 在C语言中用于网络编程的 struct sockaddr
是一个通用的地址结构,用于处理各种类型的套接字地址。这个结构通常与专用的地址结构一起使用,如 struct sockaddr_in
(用于IPv4地址)和 struct sockaddr_in6
(用于IPv6地址)。由于历史原因和向后兼容性,struct sockaddr
被广泛用于套接字函数中,如 bind()
, connect()
, accept()
等。
struct sockaddr
的定义 ;
struct sockaddr
在 <sys/socket.h>
头文件中定义,如下所示:
1 2 3 4 struct sockaddr { sa_family_t sa_family; char sa_data[14 ]; };
为什么使用struct sockaddr
struct sockaddr
本身并不常直接用于存储地址信息 ,因为它不提供足够的空间来存储具体的地址详情。相反,更具体的结构 如 struct sockaddr_in
用于IPv4地址,而 struct sockaddr_in6
用于IPv6地址。这些结构包含了特定于协议的地址信息,如IP地址和端口号
当调用套接字函数时,这些特定的地址结构被转换 为 struct sockaddr
类型的指针,以便提供一个通用的接口。例如,bind()
函数接受 struct sockaddr *
类型的参数,尽管实际传递的可能是 struct sockaddr_in *
类型的地址
struct sockaddr_in
的定义
对于IPv4网络编程,通常使用 struct sockaddr_in
,它在 <netinet/in.h>
中定义,如下所示:
1 2 3 4 5 6 struct sockaddr_in { short sin_family; unsigned short sin_port; struct in_addr sin_addr ; char sin_zero[8 ]; };
其中 struct in_addr
结构仅包含一个无符号长整型的IP地址。
struct in_addr
类型的 sin_addr
成员用于存储IPv4网络地址。struct in_addr
是专门设计来容纳IPv4地址的结构
struct in_addr
的定义 :
在 <netinet/in.h>
头文件中,struct in_addr
通常被定义如下:
1 2 3 4 struct in_addr { uint32_t s_addr; };
s_addr
是一个无符号的32位整数,用于存储IPv4地址。在网络通信中,IPv4地址通常以网络字节序(大端)存储。s_addr
是一个无符号的32位整数,用于存储IPv4地址。在网络通信中,IPv4地址通常以网络字节序(大端)存储。
使用struct in_addr
在使用 struct sockaddr_in
结构时,你通常会通过操作 sin_addr
的 s_addr
成员来指定IPv4地址。以下是一些常见的用法:
指定任意地址 :使用 INADDR_ANY
,它通常被定义为 0.0.0.0
,这表示套接字可以绑定到机器上的任何IP地址
1 addr.sin_addr.s_addr = htonl(INADDR_ANY);
指定具体的IP地址 :使用 inet_addr()
函数将点分十进制的IP地址字符串转换为合适的格式
1 addr.sin_addr.s_addr = inet_addr("192.168.1.1" );
或者使用 inet_aton()
函数,这是一个更健壮的选择
1 2 3 4 struct sockaddr_in addr ;struct in_addr ip ;inet_aton("192.168.1.1" , &ip); addr.sin_addr = ip;
inet_addr() inet_addr()
函数用于将一个点分十进制的IPv4地址字符串转换为一个网络字节序的整数值。这个函数是处理IPv4地址的传统方法,但它已经被更现代和灵活的 inet_pton()
函数所取代。尽管如此,inet_addr()
仍然在许多程序中使用
inet_addr()
函数的原型定义在 <arpa/inet.h>
头文件中,如下所示:
1 2 3 #include <arpa/inet.h> in_addr_t inet_addr (const char *cp) ;
cp
:指向以零结尾的字符数组的指针,这个数组包含了一个点分十进制的IP地址(如 “192.168.1.1”)。
返回值 :
成功时,函数返回一个无符号的32位整数,表示IPv4地址的网络字节序。
失败时,如果输入不是有效的IPv4地址,它返回 INADDR_NONE
,通常等于 -1
(或 0xFFFFFFFF
)。
inet_pton() inet_pton()
函数是用于将点分十进制的IP地址(如 “192.168.1.1”)转换为网络字节序的二进制形式的函数。这个函数是 inet_addr()
的一个更现代和通用的替代,它支持IPv4和IPv6地址
inet_pton()
函数的原型定义在 <arpa/inet.h>
头文件中,如下所示:
1 2 3 #include <arpa/inet.h> int inet_pton (int af, const char *src, void *dst) ;
af
:指定地址族,AF_INET
用于IPv4地址,AF_INET6
用于IPv6地址。
src
:指向要转换的以零结尾的字符数组(即C字符串)的指针,这个数组包含了一个点分十进制的IP地址(对于IPv4)或一个十六进制的IP地址(对于IPv6)。
dst
:指向一个足够大的缓冲区的指针,该缓冲区用于存储转换后的网络地址。对于IPv4,这通常是一个 struct in_addr
;对于IPv6,这通常是一个 struct in6_addr
。
返回值 :
返回 1
表示转换成功。
返回 0
表示 src
不包含有效的IP地址。
返回 -1
表示发生错误,errno
会设置为相应的错误代码。
inet_ntop() inet_ntop
函数是一种在C语言中用于网络编程的函数,它用于将网络地址(如IPv4或IPv6地址)从其数值形式转换为标准文本形式(如点分十进制格式的IPv4地址或冒号分隔格式的IPv6地址)。与旧的 inet_ntoa
函数相比,inet_ntop
提供了更大的灵活性和安全性,它支持IPv6,并且是线程安全的。
函数原型:
1 2 3 #include <arpa/inet.h> const char *inet_ntop (int af, const void *src, char *dst, socklen_t size) ;
参数说明:
af
: 地址族,AF_INET
用于IPv4地址,AF_INET6
用于IPv6地址。
src
: 指向一个包含要转换的网络地址的缓冲区的指针。
dst
: 指向目的缓冲区的指针,这里将存储转换后的文本地址。
size
: 目的缓冲区的大小
返回值:
如果函数成功,返回一个指向结果字符串的指针。如果发生错误,则返回 NULL
注意 :
使用 inet_ntop
时,您需要提供足够大的缓冲区来存储转换后的地址。对于IPv4,这个缓冲区应至少为16个字符的长度(包括终止空字符)。对于IPv6,建议的最小大小是46个字符。
listen() listen
函数的作用是启动监听来自客户端的连接请求
函数原型
1 2 3 #include <sys/types.h> #include <sys/socket.h> int listen (int sockfd, int backlog) ;
参数:
sockfd
:这是由之前调用 socket
函数得到的套接字文件描述符。
backlog
:这个参数指定了队列中最多可以容纳的等待接受的连接请求的数量。它决定了同时可以有多少个客户端处于连接等待状态。
返回值:
如果函数调用成功,它会返回0。
如果调用失败,会返回-1,并设置相应的错误码,可以通过 errno
获取错误详情。
使用流程 :
创建套接字:使用 socket
函数创建一个套接字
绑定地址:使用 bind
函数将套接字与特定的IP地址和端口号绑定
监听连接:使用 listen
函数开始监听连接请求
接受连接:使用 accept
函数接受来自客户端的连接
accept() 套接字编程的 accept
函数用于服务器端接受来自客户端的连接请求 。一旦 listen
函数在服务器端套接字上启动了对连接请求的监听,accept
函数就可以用来接受这些请求
函数原型
1 2 3 #include <sys/types.h> #include <sys/socket.h> int accept (int sockfd, struct sockaddr *addr, socklen_t *addrlen) ;
参数:
sockfd
:这是服务器端监听的套接字的文件描述符,之前由 socket
函数创建并通过 bind
和 listen
函数设置
addr
:这是一个指向 struct sockaddr
的指针,用于接收一个返回值,表示连接到服务器的客户端的地址。该参数可以设置为 NULL
,如果你不需要获取客户端地址。
addrlen
:这是一个指向 socklen_t
类型变量的指针,初始时应该设置为指向一个值,表示 addr
结构的大小。函数返回时,这个值会被更新为实际接收到的地址的大小。如果 addr
是 NULL
,addrlen
也应该是 NULL
返回值:
成功时返回一个新的套接字文件描述符,用于与连接的客户端进行通信。
失败时返回-1,并设置 errno
以指示错误原因
connect() 接字编程的 connect
函数用于客户端与服务器端建立连接 。这个函数在客户端套接字上调用,指定服务器的地址和端口,以便建立到服务器的连接。
函数原型
1 2 3 #include <sys/types.h> #include <sys/socket.h> int connect (int sockfd, const struct sockaddr *addr, socklen_t addrlen) ;
参数;
sockfd
:这是一个套接字文件描述符,之前由 socket
函数创建。
addr
:这是一个指向 struct sockaddr
的指针,它包含了目的地(服务器)的地址和端口信息。对于IPv4,通常使用 struct sockaddr_in
来设置该参数,并将其转换为 struct sockaddr
类型的指针进行传递。
addrlen
:这是 addr
参数所指向的地址结构的大小
返回值
成功时返回0。
失败时返回-1,并设置 errno
以指示错误原因。
send()—TCP 套接字编程的 send
函数用于向一个已连接的套接字发送数据。这个函数常用于客户端和服务器端的套接字通信中
函数原型
1 2 3 #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
:这是指定调用执行方式的标志位集合。常用的标志包括 MSG_OOB
(发送带外数据),MSG_DONTROUTE
(不使用路由表),和 MSG_NOSIGNAL
(防止发送信号)。在大多数情况下,这个值可以设为0。
返回值:
成功时返回实际发送的字节数。
失败时返回-1,并设置 errno
以指示错误原因。
recv()—TCP 套接字编程的 recv
函数用于从一个已连接的套接字接收数据 。这个函数常用于客户端和服务器端的套接字通信中,用于读取从另一端发送过来的数据
函数原型
1 2 3 #include <sys/types.h> #include <sys/socket.h> ssize_t recv (int sockfd, void *buf, size_t len, int flags) ;
参数原型
sockfd
:这是一个套接字文件描述符,表示已经建立连接的套接字。
buf
:这是一个指针,指向用于接收数据的缓冲区。
len
:这是缓冲区的大小,指定最多可以接收的字节数。
flags
:这是指定调用执行方式的标志位集合。常用的标志包括 MSG_PEEK
(查看数据但不从队列中移除)和 MSG_WAITALL
(等待所有请求的数据到达才返回)。在大多数情况下,这个值可以设为0。
返回值
成功时返回接收到的字节数。如果连接已经结束,返回0。
失败时返回-1,并设置 errno
以指示错误原因。
sendto()—UDP 在C语言中,sendto
函数是用于发送数据到特定目的地的网络编程函数,特别是在使用无连接的网络协议(如UDP)时 。该函数允许你发送数据到指定的地址和端口,这在UDP通信中是必需的,因为UDP是一种无连接的协议,不会维持一个持久的连接状态。
函数原型:
1 2 3 4 #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
: 指向包含目的地址(如对方的IP地址和端口)的 struct sockaddr
结构的指针。
addrlen
: dest_addr
指向的结构的大小。
返回值:
如果函数成功,返回发送的字节数。如果出现错误,则返回-1,并设置相应的错误码。
使用场景 :
recvfrom()—UDP recvfrom
函数是用于从网络套接字接收数据的函数,特别是在使用无连接的网络协议(如UDP)时。这个函数不仅接收数据,还可以获取发送方的地址信息(UDP是无连接的需要记住发送方是谁 ),这在UDP通信中非常重要,因为UDP是无连接的,不会保持与发送方的持续连接状态。
函数原型:
1 2 3 4 5 #include <sys/types.h> #include <sys/socket.h> 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
: 这是一个指向 struct sockaddr
结构的指针,用于存储发送方的地址信息。
addrlen
: 这是一个指向 socklen_t
变量的指针,该变量在调用之前应该被初始化为 src_addr
指向的结构的大小。调用完成后,它将被设置为实际地址结构的大小。
返回值:
recvfrom
函数返回接收到的字节数,或者在出错时返回-1。
使用场景 :
UDP套接字 : 通常在UDP服务器和客户端中使用recvfrom
来接收数据包,因为UDP是无连接的。
错误处理 : 通过检查返回值和错误代码来处理可能发生的网络错误。
close() 关闭套接字
(2)特殊地址0.0.0.0 在网络编程中,IP地址 0.0.0.0
有一个特殊的含义。它通常不用作一个特定的目标地址,而是用于表示一个“不指定的”或“任意的”地址。这个地址的具体含义取决于其使用的上下文
在服务器端 :
当一个服务器程序绑定到IP地址 0.0.0.0
时,它表明该服务器将接受针对主机的所有IPv4地址的网络连接。这对于多网卡(多接口)的机器特别有用,因为它允许服务器在所有网络接口上监听,而不需要为每个接口指定一个单独的IP地址。
例如,如果一台服务器有两个网络接口,一个绑定了IP地址 192.168.1.5
,另一个绑定了 10.0.0.5
,那么绑定到 0.0.0.0
将允许该服务器在两个接口上都接受连接。
在客户端 :
在客户端使用 0.0.0.0
时,它通常表示“任意IPv4地址”或“从任何网络接口出去”。然而,实际中,客户端很少使用这个地址,因为在发起网络连接时,通常需要指定一个明确的目的地址。
在路由表中 :
在路由表中,0.0.0.0
通常用来表示默认路由(也称为“缺省网关”)。这意味着,如果没有为特定目的地找到更具体的路由,数据包将被发送到这个默认路由。
因此,0.0.0.0
在不同的场景中有不同的用途,但总的来说,它代表了一种“任何”或“通用”的概念,在网络配置和编程中具有重要意义。
3.报式套接字(UDP) 报式套接字(Datagram Socket)是一种在网络通信中使用的套接字类型,它提供了无连接的、不可靠的、独立的消息传输。报式套接字通常与用户数据报协议(UDP)一起使用,适用于那些不需要建立连接、可以容忍一定丢包、追求传输效率的场景 ,如视频会议、在线游戏、实时系统等。
报式套接字主要特性 :
无连接 :数据传输前不需要建立连接;每个数据包(报文)独立发送,彼此之间没有依赖关系
不可靠性 :不保证数据包的可靠到达,可能会丢失、重复或乱序;没有确认机制和重传机制
数据有边界 :数据包作为独立单元发送,每个包的边界被保留;接收方能够识别出单独的数据包
效率高 :由于缺乏连接建立和维持、无需确认和重传机制,使得通信效率较高
创建报式套接字 :
1 int sockfd = socket(AF_INET, SOCK_DGRAM, 0 );
这里,AF_INET
指定使用IPv4地址,SOCK_DGRAM
指定使用报式套接字,0
表示默认协议(通常是UDP)
使用报式套接字 :
发送数据 :使用 sendto()
函数,指定目的地址和端口,将数据包发送出去
接收数据 :使用 recvfrom()
函数,从套接字接收数据包
收发双方的步骤 :
被动端(先收包的一方,先运行):
取得SOCKET
给SOCKET取得地址(bind())
收/发消息
关闭SOCKET
主动端:
取得SOCKET
给SOCKET取得地址(可省略)
发/收消息
关闭SOCKET
(1)注意 被动端的第二步,给SOCKET取得地址,无法省略,因为需要告知发送方,自己的IP与端口才可以用于接收信息
主动端的第二步,给SOCKET取得地址,可以省略,若进行省略,则可以得到机器当前空闲的端口进行使用,而ip则是主机ip
(2)报式套接字传输实例 通信协议 使用UDP套接字进行数据通信传输
proto.h
通信协议头文件,在头文件中定义接收方使用的端口,以及通信所使用的数据格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #ifndef BASIC_PROTO_H #define BASIC_PROTO_H #define RCVPORT "1989" #define NAMESIZE 11 struct msg_st { uint8_t name[NAMESIZE]; uint32_t math; uint32_t chinese; }__attribute__((packed)); #endif
接收方源码 rever.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include "proto.h" #define IPSTRSIZE 1024 int main () { int sd; struct msg_st recvbuf ; struct sockaddr_in laddr ; struct sockaddr_in raddr ; socklen_t raddr_len; char ipstr[IPSTRSIZE]; laddr.sin_family = AF_INET; laddr.sin_port = htons(atoi(RCVPORT)); inet_pton(AF_INET,"0.0.0.0" ,&laddr.sin_addr); sd = socket(AF_INET,SOCK_DGRAM,0 ); if (sd < 0 ) { perror("socket()" ); exit (1 ); } if (bind(sd,(void *)&laddr,sizeof (laddr)) < 0 ) { perror("bind()" ); exit (1 ); } raddr_len = sizeof (raddr); while (1 ) { recvfrom(sd,&recvbuf,sizeof (recvbuf),0 ,(void *)&raddr,&raddr_len); inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE); printf ("---MESSAGE FROM %s:%d---\n" ,ipstr, ntohs(raddr.sin_port)); printf ("name:%s\n" ,recvbuf.name); printf ("chinese:%d\n" ,ntohl(recvbuf.chinese)); printf ("math:%d\n" ,ntohl(recvbuf.math)); } close(sd); exit (0 ); }
接收方(被动方先运行),编译接收方源码,运行
使用指令查看机器网络状态
1 $ netstat -anu // netstat -aut 查看tcp通信情况
可以看到,0.0.0.0;1989
在运行(地址与端口)
发送方源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #include "proto.h" int main (int argc,char *argv[]) { int sd; struct msg_st sbuf ; struct sockaddr_in raddr ; if (argc < 2 ) { fprintf (stderr ,"USage...\n" ); exit (1 ); } sd = socket(AF_INET,SOCK_DGRAM,0 ); if (sd < 0 ) { perror("scoket()" ); exit (1 ); } strcpy (sbuf.name,"ZXZ" ); sbuf.math = htonl(rand()%100 ); sbuf.chinese = htonl(rand()%100 ); raddr.sin_family = AF_INET; raddr.sin_port = htons(atoi(RCVPORT)); inet_pton(AF_INET,argv[1 ],&raddr.sin_addr); if (sendto(sd,&sbuf, sizeof (sbuf),0 ,(void *)&raddr, sizeof (raddr)) < 0 ) { perror("sendto()" ); exit (1 ); } puts ("Send successful!" ); close(sd); exit (0 ); }
编译运行
1 2 $ make snder $ ./snder 127.0.0.1 // 127.0.0.1 本地ip 因为是在该实例是在一个机器内运行 本机运行
运行结果 可以发现接收方接收信息显示如下:
4.多点通讯 多点通讯属于报式套接字,流式套接字是点对点的通信
(1)广播(全网广播\子网广播) 255.255.255.255 地址 255.255.255.255
在计算机网络中是一个特殊的广播地址。这个地址用于网络广播,它允许一台计算机发送信息给同一子网内的所有其他设备。在使用这个地址时,数据包会被发送到与发送者在同一本地网络(LAN)内的所有接收者
值得注意的是,这个地址仅限于本地网络使用,它不会被路由器转发到互联网上。这意味着,使用这个地址的信息只能在同一子网内的设备之间传播。在更广泛的网络应用中,这个地址并不用于公共互联网通信。
Socket options 在计算机网络中,Socket Options
是用于修改套接字行为的一系列配置。套接字是网络通信中用于发送和接收数据的端点。在编程中,尤其是在TCP/IP(传输控制协议/互联网协议)栈的上下文中,可以设置各种套接字选项,以控制其行为和性能 。这些选项包括但不限于:
O_REUSEADDR :允许重用本地地址和端口。这对于服务器快速重启非常有用,可以立即绑定到相同的地址和端口。
SO_KEEPALIVE :启用保活机制。如果在一定时间内没有数据交换,TCP将自动发送保活探测包。
SO_LINGER :控制套接字关闭的行为。它可以用来确保数据完全发送或设置套接字关闭前的超时时间。
SO_RCVBUF 和 SO_SNDBUF :分别设置接收和发送缓冲区的大小。这对于控制数据传输的性能很重要。
SO_TIMEOUT :设置套接字操作的超时时间,如接收和发送数据。
TCP_NODELAY (针对TCP套接字):禁用Nagle算法来减少发送延迟。这对于需要低延迟的应用很有用,如实时游戏或音视频通信。
SO_BROADCAST :设置或获取广播标志。启用时,数据报套接字被允许发送数据包到一个广播地址。这个选项对面向流的套接字没有影响。(打开这个开关则可以实现广播 ,但是没有打开可能也可以接收到广播地址的数据包,这个属于未定义行为)
IP_MULTICAST_IF :是一个套接字选项(socket option),用于指定用于传出多播数据包的接口。在多播通信中,当你的设备有多个网络接口时,IP_MULTICAST_IF
允许你选择其中一个接口用于发送多播流量。这在服务器端编程中尤其有用,因为它确保多播数据包通过正确的网络接口发送。
setsockopt() setsockopt()
函数用于设置套接字的选项,以控制其行为。这个函数是 Socket 编程接口的一部分,广泛用于网络通信程序中。setsockopt()
函数允许程序员设置各种套接字级别的选项,例如 TCP/IP 协议的参数、缓冲区大小、超时设置等
函数原型 :
1 2 #include <sys/socket.h> int setsockopt (int sockfd, int level, int optname, const void *optval, socklen_t optlen) ;
参数 :
sockfd :这是要设置选项的套接字的文件描述符。
level :指定选项所在的协议层 。例如,要设置 TCP 协议的选项,可以使用 IPPROTO_TCP
。对于通用的套接字选项,使用 SOL_SOCKET
。
optname :这是你想要设置的选项。例如,SO_REUSEADDR
、SO_RCVBUF
等。
optval :指向包含新选项值的缓冲区的指针(这个参数的确切类型和内容取决于参数3,用户想要设置的具体套接字选项 ,对于布尔类型的选项,通常设置为1表示启用或0表示禁用,如SO_BROADCAST,允许发送广播消息,就是bool类型的选项)
optlen :optval 缓冲区的大小。
返回值:
成功时,setsockopt()
返回 0
失败时返回 -1,并设置 errno
以指示错误
getsockopt() getsockopt()
函数用于获取套接字的当前选项设置。这个函数是Socket编程接口的一部分,用于检索套接字选项的状态,如缓冲区大小、超时设置等
函数原型
1 2 #include <sys/socket.h> int getsockopt (int sockfd, int level, int optname, void *optval, socklen_t *optlen) ;
参数 :
sockfd :这是要获取选项的套接字的文件描述符。
level :指定选项所在的协议层。例如,要获取 TCP 协议的选项,可以使用 IPPROTO_TCP
。对于通用的套接字选项,使用 SOL_SOCKET
。
optname :这是你想要获取的选项。例如,SO_REUSEADDR
、SO_RCVBUF
等。
optval :指向一个缓冲区的指针,在这个缓冲区中,getsockopt()
会存放获取的选项值。
optlen :一开始是一个指向包含 optval 缓冲区大小的变量的指针。当 getsockopt()
返回时,这个变量被设置为实际获取的选项值的大小。
返回值
成功时,getsockopt()
返回 0;
失败时返回 -1,并设置 errno
以指示错误
if_nametoindex() if_nametoindex
是一个在 Unix 和类 Unix 系统(比如 Linux)上常用的函数,它用于将网络接口名称(如 “eth0”、”wlan0” 等)转换为对应的接口索引号。这个接口索引号通常用于网络编程中,特别是在处理多播或原始套接字时。
函数原型
1 2 #include <net/if.h> unsigned int if_nametoindex (const char *ifname) ;
参数:
返回值:
ip ro sh ubuntu终端命令 ip ro sh
是 ip route show
的缩写形式。这个命令用于显示系统的路由表信息 。路由表是操作系统用来决定如何将数据包从一个网络传输到另一个网络的规则集。
具体来说,这个命令可以提供以下信息:
目的网络 :显示数据包的目的地网络和掩码。
网关 :数据包被发送到的下一个跳跃点(如果适用)。
源地址 :出站数据包的源地址(如果有特定路由策略)。
使用的接口 :用于发送数据包的网络接口,例如 eth0,
wlan0
等。
举个例子,如果你在终端中运行 ip ro sh
,可能会看到类似这样的输出:
1 2 default via 192.168 .1 .1 dev eth0 192.168 .1 .0 /24 dev eth0 proto kernel scope link src 192.168 .1 .2
在这个例子中:
第一行表示默认路由(default
),所有不属于本地子网的流量都会通过这个路由发送。这里的默认网关是 192.168.1.1
,通过 eth0
(以太网接口)接口。
第二行描述了本地子网的路由。它显示了子网 192.168.1.0/24
是直接通过 eth0
接口可达的,本机的IP地址是 192.168.1.2
。
(2)广播实例程序 通信协议 proto.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #ifndef BASIC_PROTO_H #define BASIC_PROTO_H #define RCVPORT "1989" #define NAMESIZE 11 struct msg_st { uint8_t name[NAMESIZE]; uint32_t math; uint32_t chinese; }__attribute__((packed)); #endif
发送方 发送方需要向广播地址,进行发包,在发送方中需要将目标地址改为255.255.255.255
网络广播地址,此外需要在程序中设置Socket Options,开启套接字广播允许标志
snder.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #include "proto.h" int main (int argc,char *argv[]) { int sd; struct msg_st sbuf ; struct sockaddr_in raddr ; sd = socket(AF_INET,SOCK_DGRAM,0 ); if (sd < 0 ) { perror("scoket()" ); exit (1 ); } int val = 1 ; if (setsockopt(sd,SOL_SOCKET,SO_BROADCAST,&(val),sizeof (val)) < 0 ) { perror("setsockopt()" ); exit (1 ); } strcpy (sbuf.name,"ZXZ" ); sbuf.math = htonl(rand()%100 ); sbuf.chinese = htonl(rand()%100 ); raddr.sin_family = AF_INET; raddr.sin_port = htons(atoi(RCVPORT)); inet_pton(AF_INET,"255.255.255.255" ,&raddr.sin_addr); if (sendto(sd,&sbuf, sizeof (sbuf),0 ,(void *)&raddr, sizeof (raddr)) < 0 ) { perror("sendto()" ); exit (1 ); } puts ("Send successful!" ); close(sd); exit (0 ); }
接收方 接收方,需要在广播地址处,进行收包操作,需要在程序中对套接字属性进行设置,开启网络广播允许标志
rever.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include "proto.h" #define IPSTRSIZE 1024 int main () { int sd; struct msg_st recvbuf ; struct sockaddr_in laddr ; struct sockaddr_in raddr ; socklen_t raddr_len; char ipstr[IPSTRSIZE]; laddr.sin_family = AF_INET; laddr.sin_port = htons(atoi(RCVPORT)); inet_pton(AF_INET,"0.0.0.0" ,&laddr.sin_addr); sd = socket(AF_INET,SOCK_DGRAM,0 ); if (sd < 0 ) { perror("socket()" ); exit (1 ); } int val = 1 ; if (setsockopt(sd,SOL_SOCKET,SO_BROADCAST,&val,sizeof (val)) < 0 ) { perror("setsockopt()" ); exit (1 ); } if (bind(sd,(void *)&laddr,sizeof (laddr)) < 0 ) { perror("bind()" ); exit (1 ); } raddr_len = sizeof (raddr); while (1 ) { recvfrom(sd,&recvbuf,sizeof (recvbuf),0 ,(void *)&raddr,&raddr_len); inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE); printf ("---MESSAGE FROM %s:%d---\n" ,ipstr, ntohs(raddr.sin_port)); printf ("name:%s\n" ,recvbuf.name); printf ("chinese:%d\n" ,ntohl(recvbuf.chinese)); printf ("math:%d\n" ,ntohl(recvbuf.math)); } close(sd); exit (0 ); }
编译运行 终端对源码进行编译
1 2 $ make rever $ make snder
打开一个终端,运行接收方
打开一个终端,运行发送方
接收方接收的消息,如图所是,发送方IP为198.168.1.106
,
终端使用命令ip ro sh
,可以知道,本机ip为 192.168.1.106
是子网192.168.1.0/24
通过以太网接口enp5s0可达的
(表示本机ip的数据包通过以太网接口eth0
发送)
(3)抓包软件wireshark使用 安装
1 $ sudo apt-get install wireshark
运行要需要root用户才可以,sudo
之后输入用户密码即可
如图:
wireshark使用实例 运行终端指令:ip ro sh
,可以发现,本机ip192.168.1.106
的数据包是从网关enp5s0
发出的。
运行./rever
可执行文件,与./snder
可执行文件
./rever
方接收的信息为如下:
信息从ip为192.168.1.106
端口为57864
发出,因此使用抓包软件,需要对enp5s0
网卡的数据进行监测
打开wireshark
软件,对网关enp5s0
进行选择
打开状态栏中的分析
中的显示过滤器表达式
,如下:
在过滤器表达式的左边字段名称
中选择,IPV4 Internet Protocol Version 4
下的ip.dst Destination Host
在关系栏中选择,==
关系,值中填写255.255.255.255
。表示的含义是snder
方发出的信息的目的ip是广播地址255.255.255.255
可以捕获的信息如下:
(4)多播\组播 多播(Multicast)和组播是网络通信中的概念,用于描述在一个网络中从一个或多个源向多个目的地高效传送信息的过程。
多播 :
定义 :多播是一种网络传输机制,它允许单个数据源同时向多个接收者发送数据(而广播是所有可能的目标是有区别的) 。这与单播(一个源到一个目标)和广播(一个源到所有可能的目标)相对。
用途 :多播主要用于节省带宽和提高效率,因为数据包只发送一次,但可以被网络中的多个接收者同时接收。这在流媒体、视频会议、实时应用程序等场景中非常有用。
如何工作 :在多播中,数据包被发送到一个特定的多播组地址。网络设备(如路由器)识别这个地址,并将数据包路由到订阅了该多播组的所有接收者。
组播
定义 :组播通常是指在特定的网络协议(如 IP 组播)中实现的多播。
IP 组播 :在 IP 网络中,组播使用特定的地址范围(例如,IPv4 中的 224.0.0.0 到 239.255.255.255)。组播地址代表一个组,数据发送到这个地址将被分发给加入该组的所有成员。
224.224.224.224 224.224.224.224
是一个 IP 地址,属于 IP 组播(IP Multicast)地址范围。IP 组播是一种网络通信机制,用于有效地将数据包从一个源发送到多个目的地。
IP 组播地址范围
IP 组播地址被定义在特定的范围内。对于 IPv4,这个范围是 224.0.0.0
到 239.255.255.255
。
这个范围内的地址用于表示网络中的一个组播组,而不是特定的单个主机。
224.224.224.224
的特殊用途
在实际中,224.224.224.224
这样的地址通常用于组播通信,比如视频流、多点会议系统,或者其他需要同时发送数据到多个接收者的应用。
不过,这个特定的地址并没有被分配给任何 “众所周知的” 组播服务(像其他一些较低的组播地址那样)。这意味着它可能被用于特定应用程序或网络的自定义用途。
(5)多播\组播实例程序 流程 :发送方,应该创建多播组,并且发送消息,而接收方则是加入发送方创建的的多播组,并且接收消息
1 2 3 4 5 6 7 8 struct ip_mreqn { struct in_addr imr_multiaddr ; struct in_addr imr_address ; int imr_ifindex; };
通信协议 在通信协议中定义,多播组号
proto.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #ifndef BASIC_PROTO_H #define BASIC_PROTO_H #define MTROUP "224.2.2.2" #define RCVPORT "1989" #define NAMESIZE 11 struct msg_st { uint8_t name[NAMESIZE]; uint32_t math; uint32_t chinese; }__attribute__((packed)); #endif
发送方 snder.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #include <net/if.h> #include "proto.h" int main (int argc,char *argv[]) { int sd; struct msg_st sbuf ; struct sockaddr_in raddr ; sd = socket(AF_INET,SOCK_DGRAM,0 ); if (sd < 0 ) { perror("scoket()" ); exit (1 ); } struct ip_mreqn mreq ; inet_pton(AF_INET,MTROUP,&mreq.imr_multiaddr); inet_pton(AF_INET,"0.0.0.0" ,&mreq.imr_address); mreq.imr_ifindex = if_nametoindex("enp5s0" ); if (setsockopt(sd,IPPROTO_IP,IP_MULTICAST_IF,&(mreq),sizeof (mreq)) < 0 ) { perror("setsockopt()" ); exit (1 ); } strcpy (sbuf.name,"ZXZ" ); sbuf.math = htonl(rand()%100 ); sbuf.chinese = htonl(rand()%100 ); raddr.sin_family = AF_INET; raddr.sin_port = htons(atoi(RCVPORT)); inet_pton(AF_INET,MTROUP,&raddr.sin_addr); if (sendto(sd,&sbuf, sizeof (sbuf),0 ,(void *)&raddr, sizeof (raddr)) < 0 ) { perror("sendto()" ); exit (1 ); } puts ("Send successful!" ); close(sd); exit (0 ); }
接收方 rever.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <net/if.h> #include "proto.h" #define IPSTRSIZE 1024 int main () { int sd; struct msg_st recvbuf ; struct sockaddr_in laddr ; struct sockaddr_in raddr ; socklen_t raddr_len; char ipstr[IPSTRSIZE]; laddr.sin_family = AF_INET; laddr.sin_port = htons(atoi(RCVPORT)); inet_pton(AF_INET,"0.0.0.0" ,&laddr.sin_addr); sd = socket(AF_INET,SOCK_DGRAM,0 ); if (sd < 0 ) { perror("socket()" ); exit (1 ); } struct ip_mreqn mreq ; inet_pton(AF_INET,MTROUP,&mreq.imr_multiaddr); inet_pton(AF_INET,"0.0.0.0" ,&mreq.imr_address); mreq.imr_ifindex = if_nametoindex("enps50" ); if (setsockopt(sd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof (mreq)) < 0 ) { perror("setsockopt()" ); exit (1 ); } if (bind(sd,(void *)&laddr,sizeof (laddr)) < 0 ) { perror("bind()" ); exit (1 ); } raddr_len = sizeof (raddr); while (1 ) { recvfrom(sd,&recvbuf,sizeof (recvbuf),0 ,(void *)&raddr,&raddr_len); inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE); printf ("---MESSAGE FROM %s:%d---\n" ,ipstr, ntohs(raddr.sin_port)); printf ("name:%s\n" ,recvbuf.name); printf ("chinese:%d\n" ,ntohl(recvbuf.chinese)); printf ("math:%d\n" ,ntohl(recvbuf.math)); } close(sd); exit (0 ); }
编译运行
分别打开终端运行接收方与发送方
5.流式套接字(TCP) 收发双方的步骤 :
被动端(S端先收包的一方,先运行):
取得SOCKET
给SOCKET取得地址
将SOCKET设置为监听模式
接受连接
收/发消息
关闭SOCKET
主动端(C端):
取得SOCKET
给SOCKET取得地址(可省略)
发送连接
发/收消息
关闭SOCKET
(1)流式套接字程序实例 通信协议 通信协议中需要确定发送端运行时候占用机器的端口号,以及服务端与客户端通信连接后,返回给客户端的信息格式FMT_STAMP
。
proto.h
1 2 3 4 5 6 7 8 9 10 11 12 #ifndef BASIC_PROTO_H #define BASIC_PROTO_H #define SERVERPORT "1989" #define FMT_STAMP "%lld\r\n" #endif
接收方(Server第一版) Server.c
第一版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #include <time.h> #include "proto.h" #define IPSTRSIZE 40 #define BUFSIZE 1024 static void server_job (int sd) { int len; char buf[BUFSIZE]; len = sprintf (buf,FMT_STAMP,(long long )time(NULL )); if (send(sd,buf,len,0 ) < 0 ) { perror("send()" ); exit (1 ); } } int main () { int sd; int new_sd; char ipstr[IPSTRSIZE]; struct sockaddr_in laddr ; struct sockaddr_in raddr ; socklen_t raddr_len; sd = socket(AF_INET,SOCK_STREAM,0 ); if (sd < 0 ) { perror("socket()" ); exit (1 ); } laddr.sin_family = AF_INET; laddr.sin_port = htons(atoi(SERVERPORT)); inet_pton(AF_INET,"0.0.0.0" ,&laddr.sin_addr); if (bind(sd,(void *)&laddr,sizeof (laddr)) < 0 ) { perror("bind()" ); exit (1 ); } if (listen(sd,200 ) < 0 ) { perror("listen()" ); exit (1 ); } raddr_len = sizeof (raddr); while (1 ) { new_sd = accept(sd,(void *)&raddr,&raddr_len); if (new_sd < 0 ) { perror("accept()" ); exit (1 ); } inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE); printf ("Client:%s:%d\n" ,ipstr, ntohs(raddr.sin_port)); server_job(new_sd); close(new_sd); } close(sd); exit (0 ); }
编译运行server.c
打开一个终端运行:
打开另外一个终端,使用指令nc
,是ubuntu自带的客户端,指定服务器ip 与 端口号即可以与服务器进行通信
客户端端接收的信息如下,接收到服务端返回的时间戳
运行服务端的终端,输出与其连接的客户端的ip与端口号
使用Crtl+C
,将服务端程序终止,再次运行会出现如下情况:
显示服务端bind
失败,需要使用的ip与端口已经为占用,这是因为,使用Ctrl+C
终端程序运行,是使用信号中断进程的运行,在服务端程序中,并没有执行到close(sd)
,关闭套接字描述符的代码,因此在一小段时间内,该套接字还是会显示打开的状态,则ip与端口就会依然被占用。一段时间过后,系统发现后,则会主动进行资源的释放与回收。
如何改进呢?如下:
接收方(Server第二版) 设置套接字的属性,如套接字在上一次使用中没有来的及释放,那么在下一次重新使用bind绑定因为没有来得及释放而被占用的ip与端口时,则会立马释放,以此来使得此次bind
成功
SO_REUSEADDR
SO_REUSEADDR
是一个套接字选项,用于在套接字编程中控制地址重用的行为。这个选项使得在同一端口上可以快速重启一个监听服务,而不必等待操作系统关闭前一个服务的套接字
在程序中添加代码:
1 2 3 4 5 6 7 int val = 1 ; if (setsockopt(sd,SOL_SOCKET,SO_REUSEADDR,&val,sizeof (val)) < 0 ) { perror("setsockopt()" ); exit (1 ); }
server.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #include <time.h> #include "proto.h" #define IPSTRSIZE 40 #define BUFSIZE 1024 static void server_job (int sd) { int len; char buf[BUFSIZE]; len = sprintf (buf,FMT_STAMP,(long long )time(NULL )); if (send(sd,buf,len,0 ) < 0 ) { perror("send()" ); exit (1 ); } } int main () { int sd; int new_sd; char ipstr[IPSTRSIZE]; struct sockaddr_in laddr ; struct sockaddr_in raddr ; socklen_t raddr_len; sd = socket(AF_INET,SOCK_STREAM,0 ); if (sd < 0 ) { perror("socket()" ); exit (1 ); } int val = 1 ; if (setsockopt(sd,SOL_SOCKET,SO_REUSEADDR,&val,sizeof (val)) < 0 ) { perror("setsockopt()" ); exit (1 ); } laddr.sin_family = AF_INET; laddr.sin_port = htons(atoi(SERVERPORT)); inet_pton(AF_INET,"0.0.0.0" ,&laddr.sin_addr); if (bind(sd,(void *)&laddr,sizeof (laddr)) < 0 ) { perror("bind()" ); exit (1 ); } if (listen(sd,200 ) < 0 ) { perror("listen()" ); exit (1 ); } raddr_len = sizeof (raddr); while (1 ) { new_sd = accept(sd,(void *)&raddr,&raddr_len); if (new_sd < 0 ) { perror("accept()" ); exit (1 ); } inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE); printf ("Client:%s:%d\n" ,ipstr, ntohs(raddr.sin_port)); server_job(new_sd); close(new_sd); } close(sd); exit (0 ); }
编译运行server.c
打开一个终端运行:
打开另外一个终端,使用指令nc
,是ubuntu自带的客户端,指定服务器ip 与 端口号即可以与服务器进行通信
客户端端接收的信息如下,接收到服务端返回的时间戳
运行服务端的终端,输出与其连接的客户端的ip与端口号
使用Crtl+C
,将服务端程序终止,再次运行会出现如下情况:
没有出现如第一版,bind失败的情形。
发送端1 Client.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #include <time.h> #include "proto.h" int main (int argc,char *argv[]) { int sd; struct sockaddr_in raddr ; long long stamp; FILE *fp; if (argc < 2 ) { fprintf (stderr ,"Usage...\n" ); exit (1 ); } sd = socket(AF_INET,SOCK_STREAM,0 ); if (sd < 0 ) { perror("socket()" ); exit (1 ); } raddr.sin_family = AF_INET; raddr.sin_port = htons(atoi(SERVERPORT)); inet_pton(AF_INET,argv[1 ],&raddr.sin_addr); if (connect(sd,(void *)&raddr,sizeof (raddr)) < 0 ) { perror("'connect()" ); exit (1 ); } fp = fdopen(sd,"r+w" ); if (fp ==NULL ) { perror("fdopen()" ); exit (1 ); } if (fscanf (fp,FMT_STAMP,&stamp) < 1 ) { fprintf (stderr ,"Bad format!\n" ); exit (1 ); } else { fprintf (stdout ,"stamp == %lld\n" ,stamp); } fclose(fp); exit (0 ); }
发送端2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #include <time.h> #include "proto.h" #define BUFSIZE 1024 int main (int argc,char *argv[]) { int sd; struct sockaddr_in raddr ; long long stamp; FILE *fp; char BUF[BUFSIZE]; if (argc < 2 ) { fprintf (stderr ,"Usage...\n" ); exit (1 ); } sd = socket(AF_INET,SOCK_STREAM,0 ); if (sd < 0 ) { perror("socket()" ); exit (1 ); } raddr.sin_family = AF_INET; raddr.sin_port = htons(atoi(SERVERPORT)); inet_pton(AF_INET,argv[1 ],&raddr.sin_addr); if (connect(sd,(void *)&raddr,sizeof raddr) < 0 ) { perror("connect()" ); exit (1 ); } if (recv(sd,BUF,sizeof BUF,0 ) < 0 ) { perror("recv()" ); exit (1 ); } stamp = strtoll(BUF,NULL ,0 ); fprintf (stderr ,"stamp = %lld\n" ,stamp); close(sd); exit (0 ); }
编译
先打开一个终端,运行服务端程序,等待客户端发送连接请求
再打开另外一个终端,运行客户端程序,发送连接请求,并且若连接成功则打印,服务端回传的信息
客户端显示:
服务端显示:
接收方(Server第三版并发版) 为了提高服务端的执行效率,服务端将接收连接以其之前的工作由父进程执行操作,而server_job
等操作交由子进程进行操作
server.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #include <time.h> #include "proto.h" #define IPSTRSIZE 40 #define BUFSIZE 1024 static void server_job (int sd) { int len; char buf[BUFSIZE]; len = sprintf (buf,FMT_STAMP,(long long )time(NULL )); if (send(sd,buf,len,0 ) < 0 ) { perror("send()" ); exit (1 ); } } int main () { int sd; int new_sd; char ipstr[IPSTRSIZE]; pid_t pid; struct sockaddr_in laddr ; struct sockaddr_in raddr ; socklen_t raddr_len; sd = socket(AF_INET,SOCK_STREAM,0 ); if (sd < 0 ) { perror("socket()" ); exit (1 ); } int val = 1 ; if (setsockopt(sd,SOL_SOCKET,SO_REUSEADDR,&val,sizeof (val)) < 0 ) { perror("setsockopt()" ); exit (1 ); } laddr.sin_family = AF_INET; laddr.sin_port = htons(atoi(SERVERPORT)); inet_pton(AF_INET,"0.0.0.0" ,&laddr.sin_addr); if (bind(sd,(void *)&laddr,sizeof (laddr)) < 0 ) { perror("bind()" ); exit (1 ); } if (listen(sd,200 ) < 0 ) { perror("listen()" ); exit (1 ); } raddr_len = sizeof (raddr); while (1 ) { new_sd = accept(sd,(void *)&raddr,&raddr_len); if (new_sd < 0 ) { perror("accept()" ); exit (1 ); } pid = fork(); if (pid <0 ) { perror("fork()'" ); exit (1 ); } if (pid == 0 ) { close(sd); inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE); printf ("[%d]:Client:%s:%d\n" ,getpid(),ipstr, ntohs(raddr.sin_port)); server_job(new_sd); close(new_sd); exit (0 ); } else { close(new_sd); } } close(sd); exit (0 ); }
编译运行
分别打开终端运行server
与Client
发送方显示:
接收方显示;
(2)静态进程池套接字实现 在之前的流式套接字实例中,若在服务端,加入进程池的机制,当父进程建立连接之后,进程池中的子进程开始对任务进行竞争,那情况如何呢?
accept
调用通常是线程安全的,并且操作系统内核会负责处理多个并发的 accept
调用。这意味着多个线程可以安全地在同一个监听套接字上调用 accept
而不会相互干扰,因此accept函数不需要额外增加信号量或者进程锁相关
接收方(Server第四版静态进程池版本) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #include <time.h> #include <sys/wait.h> #include "proto.h" #define IPSTRSIZE 40 #define BUFSIZE 1024 #define PROCNUM 4 static void server_loop (int sd) ;static void server_job (int sd) { int len; char buf[BUFSIZE]; len = sprintf (buf,FMT_STAMP,(long long )time(NULL )); if (send(sd,buf,len,0 ) < 0 ) { perror("send()" ); exit (1 ); } } int main () { int sd,i; int new_sd; char ipstr[IPSTRSIZE]; struct sockaddr_in laddr ; pid_t pid; sd = socket(AF_INET,SOCK_STREAM,0 ); if (sd < 0 ) { perror("socket()" ); exit (1 ); } int val = 1 ; if (setsockopt(sd,SOL_SOCKET,SO_REUSEADDR,&val,sizeof (val)) < 0 ) { perror("setsockopt()" ); exit (1 ); } laddr.sin_family = AF_INET; laddr.sin_port = htons(atoi(SERVERPORT)); inet_pton(AF_INET,"0.0.0.0" ,&laddr.sin_addr); if (bind(sd,(void *)&laddr,sizeof (laddr)) < 0 ) { perror("bind()" ); exit (1 ); } if (listen(sd,200 ) < 0 ) { perror("listen()" ); exit (1 ); } for (i=0 ;i<PROCNUM;i++) { pid = fork(); if (pid < 0 ) { perror("fork()" ); exit (1 ); } if (pid == 0 ) { server_loop(sd); close(sd); exit (0 ); } } for (i=0 ;i<PROCNUM;i++) wait(NULL ); close(sd); exit (0 ); } static void server_loop (int sd) { int new_sd; struct sockaddr_in raddr ; socklen_t raddr_len; char ipstr[IPSTRSIZE]; raddr_len = sizeof (raddr); while (1 ) { new_sd = accept(sd,(void *)&raddr,&raddr_len); if (new_sd < 0 ) { perror("accept()" ); exit (1 ); } inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE); printf ("[%d]--Client:%s:%d\n" ,getpid(),ipstr, ntohs(raddr.sin_port)); server_job(new_sd); close(new_sd); } }
编译运行还是正常的
(3)套接字动态进程池实现 进程池中,父进程需要统筹管理进程池中的资源,当存在有子进程状态发生变化时,就要对进程池中的资源进行扫描,发现存在资源被空闲占用时,就需要将空闲的子进程杀死,释放其占用的资源。当发现存在此时进程池中子进程数量少于最小值时,需要创建子进程满足数量最小值,并执行任务
主进程每次间隔3s,就会扫描进程池,更新当前忙碌的子进程数量与空闲的子进程数量,当当前空闲的数量小于MINSPARESERVER,则会继续创建子进程使得存在子进程调用accept函数,等待客户端的连接。而不是每次发现有客户端发送连接请求了之后在去创建子进程。而且一开始就创建好空闲的子进程使用accept阻塞等待客户端连接请求
进程池还有一个思路:专门创建一个子进程用于accept函数阻塞等待客户端请求,当服务端连接上客户端,则创建其他子进程用于与客户端通信收发数据。
server.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #include <time.h> #include <sys/wait.h> #include <sys/mman.h> #include <errno.h> #include "proto.h" #define MINSPARESERVER 5 #define MAXSPARESERVER 10 #define MAXCLIENTS 20 #define IPSTRSIZE 40 #define LINEBUFSIZE 2048 #define SIG_NOTIFY SIGUSR2 enum { STATE_IDEL = 0 , STATE_BUSY }; struct server_st { pid_t pid; int state; }; static struct server_st *serverpool ;static int idle_count=0 ,busy_count = 0 ;static int sd;static void usr2_handler (int s) { return ; } static void server_job (int pos) { struct sockaddr_in raddr ; socklen_t raddr_len; int ppid; int client_sd; time_t stamp; int len; char ipstr[IPSTRSIZE]; char linebuf[LINEBUFSIZE]; ppid = getppid(); raddr_len = sizeof raddr; while (1 ) { client_sd = accept(sd,(void *)&raddr,&raddr_len); if (client_sd <0 ) { if (errno != EINTR || errno != EAGAIN) { perror("accept()" ); exit (1 ); } } serverpool[pos].state = STATE_BUSY; kill(ppid,SIG_NOTIFY); inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE); stamp = time(NULL ); len = snprintf (linebuf,LINEBUFSIZE,FMT_STAMP,stamp); send(client_sd,linebuf,len,0 ); sleep(5 ); close(client_sd); serverPool[pos].state = STATE_IDLE; kill(ppid,SIG_NOTIFY); } } static int add_1_server (void ) { pid_t pid; int slot; if (idle_count + busy_count >= MAXCLIENTS) return -1 ; for (slot = 0 ;slot<MAXCLIENTS;slot++) if (serverpool[slot].pid == -1 ) break ; serverpool[slot].state = STATE_IDEL; pid = fork(); if (pid < 0 ) { perror("fork()" ); exit (1 ); } if (pid == 0 ) { server_job(slot); exit (1 ); } else { serverpool[slot].pid = pid; idle_count++; } return 0 ; } static int del_1_server (void ) { int i; if (idle_count==0 ) return -1 ; for (i=0 ;i<MAXCLIENTS;i++) { if (serverpool[i].pid != -1 && serverpool[i].state == STATE_IDEL) { kill(serverpool[i].pid,SIGTERM); serverpool[i].pid=-1 ; idle_count--; break ; } } } static int scan_pool () { int i; int busy = 0 ,idle = 0 ; for (i = 0 ;i<MAXCLIENTS;i++) { if (serverpool[i].pid == -1 ) continue ; if (kill(serverpool[i].pid,0 )) { serverpool[i].pid = -1 ; continue ; } if (serverpool[i].state == STATE_IDEL) { idle++; }else if (serverpool[i].state == STATE_BUSY){ busy++; } else { fprintf (stderr ,"Unknown state\n" ); abort (); } } idle_count = idle; busy_count = busy; return 0 ; } int main (int argc,char *argv[]) { struct sockaddr_in laddr ; int i; struct sigaction sa ,osa ; sa.sa_handler = SIG_IGN; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_NOCLDWAIT; sigaction(SIGCHLD,&sa,&osa); sa.sa_handler = usr2_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0 ; sigaction(SIG_NOTIFY,&sa,&osa); sigset_t set ,oset; sigemptyset(&set ); sigaddset(&set ,SIG_NOTIFY); sigprocmask(SIG_BLOCK,&set ,&oset); serverpool = mmap(NULL ,sizeof (struct server_st)*MAXCLIENTS,PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1 , 0 ); if (serverpool == MAP_FAILED) { perror("mmap()" ); exit (1 ); } for (i = 0 ;i<MAXCLIENTS;i++) { serverpool[i].pid = -1 ; } sd = socket(AF_INET,SOCK_STREAM,0 ); if (sd < 0 ) { perror("socket()" ); exit (1 ); } int val = 1 ; if (setsockopt(sd,SOL_SOCKET,SO_REUSEADDR,&val,sizeof (val)) < 0 ) { perror("setsockopt()" ); exit (1 ); } laddr.sin_family = AF_INET; laddr.sin_port = htons(atoi(SERVERPORT)); inet_pton(AF_INET,"0.0.0.0" ,&laddr.sin_addr); if (bind(sd,(void *)&laddr,sizeof (laddr)) < 0 ) { perror("bind()" ); exit (1 ); } if (listen(sd,100 ) < 0 ) { perror("listen()" ); exit (1 ); } for (i = 0 ;i<MINSPARESERVER;i++) { add_1_server(); } printf ("This ServerPool Running...\n" ); while (1 ) { sigsuspend(&oset); scan_pool(); if (idle_count > MAXSPARESERVER) { for (i = 0 ;i<(idle_count-MAXSPARESERVER);i++) { del_1_server(); } } else if (idle_count < MINSPARESERVER) { for (i = 0 ;i<(MINSPARESERVER-idle_count);i++) { add_1_server(); } } for (i = 0 ;i<MAXCLIENTS;i++) { if (serverpool[i].pid == -1 ) putchar ('s' ); else if (serverpool[i].state == STATE_IDEL) putchar ('.' ); else putchar ('x' ); } putchar ('\n' ); } sigprocmask(SIG_SETMASK,&oset,NULL ); munmap(serverPool, sizeof (struct server_st) * MAXCLIENTS); exit (0 ); }
编译运行启动server端
另开一个终端,运行
1 2 3 $ while true ; do (./client 127.0.0.1 &);sleep 1;done // 每秒运行一次指令 ./client 或者 $ while true ; do (./client 0.0.0.0 &);sleep 1;done // 每秒运行一次指令 ./client
服务端显示进程池中各个子进程的运行状态