进程的基本概念
- 定义:在Linux系统中,进程是正在执行的一个程序实例,它是资源分配和调度的基本单位。每个进程都有自己独立的地址空间、数据段、代码段、栈以及一组系统资源(如文件描述符、内存等)。
- 进程的组成部分 :
- 代码段(Text Segment):包含程序的可执行代码,通常是只读的,多个进程可以共享相同程序的代码段。
- 数据段(Data Segment):存储程序中已初始化的全局变量和静态变量。
- BSS段(Block Started by Symbol):存放程序中未初始化的全局变量和静态变量,在程序加载时,系统会将BSS段初始化为全零。
- 栈(Stack):用于存储函数调用的局部变量、函数参数、返回地址等信息。栈的增长方向是从高地址向低地址。
- 堆(Heap) :用于动态分配内存,程序可以在运行时通过
malloc
等函数在堆上申请内存,堆的增长方向是从低地址向高地址。
- 进程的创建 -
fork
函数- 函数原型 :
pid_t fork(void);
,其中pid_t
是一个整数类型,用于表示进程ID。 - 工作原理 :当一个进程调用
fork
函数时,系统会创建一个新的进程,这个新进程几乎是原进程的一个副本。原进程称为父进程,新创建的进程称为子进程。子进程会复制父进程的代码段、数据段、堆和栈等资源。 - 返回值 :
- 在父进程中,
fork
函数返回新创建的子进程的进程ID。这个ID是一个大于0的值,用于在父进程中区分不同的子进程。 - 在子进程中,
fork
函数返回0。 - 如果
fork
函数调用失败,会返回 - 1,并设置errno
来指示错误原因,例如内存不足等。
- 在父进程中,
- 示例代码:
- 函数原型 :
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
pid_t pid;
pid = fork();
if (pid == -1) {
perror("fork失败");
return 1;
} else if (pid == 0) {
// 子进程代码
printf("这是子进程,进程ID为 %d,父进程ID为 %d\n", getpid(), getppid());
} else {
// 父进程代码
printf("这是父进程,进程ID为 %d,子进程ID为 %d\n", getpid(), pid);
}
return 0;
}
- 进程的终止
- 正常终止方式 :
return
语句(在main
函数中) :当main
函数执行return
语句时,进程会正常终止。返回值可以被父进程获取(如果父进程有获取子进程返回值的机制),用于表示进程的执行结果。exit
函数 :void exit(int status);
,其中status
是进程的退出状态码,用于告知父进程本进程的退出状态。exit
函数会执行一些清理工作,如关闭文件描述符、刷新标准I/O缓冲区等,然后终止进程。
- 异常终止方式 :
abort
函数 :会导致进程异常终止,并产生一个SIGABRT
信号,用于在程序出现严重错误(如无法恢复的错误)时强行终止进程。- 收到信号终止 :进程可以接收来自操作系统或其他进程发送的信号而终止。例如,当用户在终端中按下
Ctrl + C
时,当前正在运行的进程会收到SIGINT
信号,如果进程没有对该信号进行处理,就会终止。
- 正常终止方式 :
- 进程等待 -
wait
和waitpid
函数wait
函数 :- 函数原型 :
pid_t wait(int *status);
,其中status
是一个指向整数的指针,用于存储子进程的退出状态信息。 - 功能 :父进程调用
wait
函数会阻塞自己,直到它的一个子进程终止。当子进程终止后,wait
函数会返回终止子进程的进程ID,并将子进程的退出状态信息存储在status
指向的变量中。
- 函数原型 :
waitpid
函数 :- 函数原型 :
pid_t waitpid(pid_t pid, int *status, int options);
,其中pid
指定要等待的子进程的进程ID,status
和wait
函数中的作用相同,options
用于设置等待选项,如WNOHANG
表示如果没有子进程终止就立即返回,不阻塞父进程。 - 功能 :相比
wait
函数更加灵活,可以等待指定的子进程,并且可以通过options
参数控制等待行为。
- 函数原型 :
- 示例代码(使用
wait
函数):
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t pid;
int status;
pid = fork();
if (pid == -1) {
perror("fork失败");
return 1;
} else if (pid == 0) {
// 子进程代码
printf("子进程开始执行,进程ID为 %d\n", getpid());
// 模拟子进程执行一些任务后退出
sleep(2);
return 42;
} else {
// 父进程代码
printf("父进程等待子进程...\n");
pid_t terminated_pid = wait(&status);
if (terminated_pid == -1) {
perror("wait失败");
return 1;
}
if (WIFEXITED(status)) {
int exit_status = WEXITSTATUS(status);
printf("子进程 %d 正常退出,退出状态为 %d\n", terminated_pid, exit_status);
}
}
return 0;
}
- 进程的执行顺序和调度
- 进程调度器:Linux系统中有进程调度器,它决定哪个进程可以在CPU上运行。调度器会根据一定的算法(如时间片轮转、优先级调度等)来分配CPU时间。
- 优先级和Nice值 :每个进程都有一个优先级,优先级高的进程会优先获得CPU时间。可以通过调整进程的
Nice
值来间接改变进程的优先级。Nice
值的范围是 - 20到19,Nice
值越小,优先级越高。可以使用nice
命令(用于启动一个具有指定Nice
值的进程)和renice
命令(用于改变一个正在运行进程的Nice
值)来操作进程的Nice
值。
- 管道(Pipe)
- 基本概念 :
- 管道是一种最基本的进程间通信方式,它是一个单向的、先进先出(FIFO)的数据通道。管道用于连接一个写进程和一个读进程,写进程将数据写入管道的一端,读进程从管道的另一端读取数据。管道通常用于具有亲缘关系(如父子进程)的进程之间通信。
- 创建和使用方式 :
- 在Linux系统中,可以使用
pipe
函数来创建一个管道。pipe
函数的原型为int pipe(int pipefd[2]);
,其中pipefd
是一个包含两个整数的数组,pipefd[0]
用于读取管道数据(管道的读端),pipefd[1]
用于向管道写入数据(管道的写端)。成功时返回0,失败时返回 - 1。
- 在Linux系统中,可以使用
- 示例代码(父子进程间通过管道通信):
- 基本概念 :
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#define BUFFER_SIZE 1024
int main() {
int pipefd[2];
pid_t pid;
char buffer[BUFFER_SIZE];
// 创建管道
if (pipe(pipefd) == -1) {
perror("创建管道失败");
return 1;
}
// 创建子进程
pid = fork();
if (pid == -1) {
perror("创建子进程失败");
return 1;
} else if (pid == 0) {
// 子进程关闭管道写端,从管道读端读取数据
close(pipefd[1]);
ssize_t num_read = read(pipefd[0], buffer, sizeof(buffer));
if (num_read == -1) {
perror("子进程读取管道数据失败");
return 1;
} else if (num_read == 0) {
printf("管道已关闭,没有数据可读。\n");
} else {
buffer[num_read] = '\0';
printf("子进程读取到的数据:%s\n", buffer);
}
close(pipefd[0]);
} else {
// 父进程关闭管道读端,向管道写端写入数据
close(pipefd[0]);
char data[] = "这是通过管道发送的数据。";
ssize_t num_written = write(pipefd[1], data, strlen(data));
if (num_written == -1) {
perror("父进程向管道写入数据失败");
return 1;
}
printf("父进程向管道写入了 %zd 个字节的数据。\n", num_written);
close(pipefd[1]);
}
return 0;
}
- 特点 :
- 简单易用,适用于父子进程等有亲缘关系的进程之间的简单数据传输。但管道是半双工通信方式,数据只能单向流动,如果要实现双向通信,需要创建两个管道。并且管道的容量有限,如果写进程写入数据的速度超过读进程读取数据的速度,管道可能会阻塞。
- 命名管道(Named Pipe或FIFO)
- 基本概念 :
- 命名管道是一种特殊类型的文件,它也提供了一个单向或双向的通信通道。与普通管道不同的是,命名管道有一个文件名,可以被多个没有亲缘关系的进程访问,用于实现这些进程之间的通信。
- 创建和使用方式 :
- 可以使用
mkfifo
命令(在命令行中)或mkfifo
函数(在程序中)来创建一个命名管道。mkfifo
函数的原型为int mkfifo(const char *pathname, mode_t mode);
,其中pathname
是命名管道的文件名,mode
是文件的权限模式。成功时返回0,失败时返回 - 1。 - 一个进程以写方式打开命名管道(使用
open
函数,打开模式为O_WRONLY
),另一个进程以读方式打开命名管道(打开模式为O_RDONLY
),就可以进行通信。
- 可以使用
- 示例代码(两个无关进程通过命名管道通信):
- 基本概念 :
c
// 写进程
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define BUFFER_SIZE 1024
int main() {
int fd;
char data[] = "这是通过命名管道发送的数据。";
// 创建命名管道(如果不存在)
if (mkfifo("myfifo", 0666) == -1 && errno!= EEXIST) {
perror("创建命名管道失败");
return 1;
}
// 以写方式打开命名管道
fd = open("myfifo", O_WRONLY);
if (fd == -1) {
perror("打开命名管道(写)失败");
return 1;
}
ssize_t num_written = write(fd, data, strlen(data));
if (num_written == -1) {
perror("向命名管道写入数据失败");
close(fd);
return 1;
}
printf("向命名管道写入了 %zd 个字节的数据。\n", num_written);
close(fd);
return 0;
}
c
// 读进程
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define BUFFER_SIZE 1024
int main() {
int fd;
char buffer[BUFFER_SIZE];
// 以读方式打开命名管道
fd = open("myfifo", O_RDONLY);
if (fd == -1) {
perror("打开命名管道(读)失败");
return 1;
}
ssize_t num_read = read(fd, buffer, sizeof(buffer));
if (num_read == -1) {
perror("从命名管道读取数据失败");
close(fd);
return 1;
} else if (num_read == 0) {
printf("命名管道已关闭,没有数据可读。\n");
} else {
buffer[num_read] = '\0';
printf("从命名管道读取到的数据:%s\n", buffer);
}
close(fd);
return 0;
}
- 特点 :
- 可以在没有亲缘关系的进程之间通信,提供了更灵活的通信方式。但同样是半双工通信,若要双向通信可能需要创建两个命名管道。命名管道在打开进行读或写操作时,如果没有对应的进程进行反向操作(如打开写时没有读进程),可能会阻塞。
- 消息队列(Message Queue)
- 基本概念 :
- 消息队列是一个由内核维护的消息链表,它允许不同进程通过发送和接收消息来进行通信。消息队列中的每个消息都有一个特定的类型,可以根据类型来接收消息,使得进程可以选择性地获取自己感兴趣的消息。
- 创建和使用方式 :
- 在Linux系统中,使用消息队列需要包含
<sys/types.h>
、<sys/ipc.h>
和<sys/msg.h>
头文件。首先,使用msgget
函数来创建或获取一个消息队列标识符。msgget
函数的原型为int msgget(key_t key, int msgflg);
,其中key
是一个键值,用于唯一标识消息队列,msgflg
用于指定创建或访问消息队列的标志。 - 进程可以使用
msgsnd
函数向消息队列发送消息,msgsnd
函数的原型为int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
,其中msqid
是消息队列标识符,msgp
是指向消息结构体的指针,msgsz
是消息的长度,msgflg
是发送标志。 - 接收消息可以使用
msgrcv
函数,其原型为int msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
,其中msgtyp
是要接收的消息类型。
- 在Linux系统中,使用消息队列需要包含
- 示例代码(两个进程通过消息队列通信):
- 基本概念 :
c
// 发送消息的进程
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#define MAX_TEXT 512
struct my_msg {
long int my_msg_type;
char some_text[MAX_TEXT];
};
int main() {
int msqid;
key_t key;
struct my_msg some_data;
// 生成一个唯一的键值
if ((key = ftok(".", 'a')) == -1) {
perror("生成键值失败");
return 1;
}
// 创建或获取消息队列
if ((msqid = msgget(key, 0666 | IPC_CREAT)) == -1) {
perror("获取消息队列失败");
return 1;
}
// 设置要发送的消息类型和内容
some_data.my_msg_type = 1;
strcpy(some_data.some_text, "这是通过消息队列发送的消息。");
// 发送消息
if (msgsnd(msqid, &some_data, sizeof(some_data.some_text), 0) == -1) {
perror("发送消息失败");
return 1;
}
printf("消息已发送。\n");
return 0;
}
c
// 接收消息的进程
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#define MAX_TEXT 512
struct my_msg {
long int my_msg_type;
char some_text[MAX_TEXT];
};
int main() {
int msqid;
key_t key;
struct my_msg some_data;
// 生成一个唯一的键值
if ((key = ftok(".", 'a')) == -1) {
perror("生成键值失败");
return 1;
}
// 创建或获取消息队列
if ((msqid = msgget(key, 0666 | IPC_CREAT)) == -1) {
perror("获取消息队列失败");
return 1;
}
// 接收消息类型为1的消息
if (msgrcv(msqid, &some_data, sizeof(some_data.some_text), 1, 0) == -1) {
perror("接收消息失败");
return 1;
}
printf("接收到的消息:%s\n", some_data.some_text);
// 标记消息队列可以被删除(当所有进程都不再使用时)
if (msgctl(msqid, IPC_RMID, NULL) == -1) {
perror("删除消息队列标记失败");
return 1;
}
return 0;
}
- 特点 :
- 消息队列克服了管道和命名管道无格式字节流的缺点,消息有类型,可以按照类型进行接收,增强了通信的灵活性。消息队列可以实现多对多的通信,多个进程可以向同一个消息队列发送和接收消息。但是消息队列的容量有限,当消息队列满时,发送进程可能会阻塞;并且消息队列的实现涉及到系统调用,效率相对较低。
- 共享内存(Shared Memory)
- 基本概念 :
- 共享内存是一种高效的进程间通信方式,它允许两个或多个进程共享一段物理内存区域。进程可以像访问自己的内存一样访问共享内存区域,这使得数据的传输速度非常快,因为不需要进行数据的复制操作(如管道和消息队列需要在用户空间和内核空间之间复制数据)。
- 创建和使用方式 :
- 在Linux系统中,使用共享内存需要包含
<sys/types.h>
、<sys/ipc.h>
和<sys/shm.h>
头文件。首先,使用shmget
函数来创建或获取一个共享内存段标识符。shmget
函数的原型为int shmget(key_t key, size_t size, int shmflg);
,其中key
是一个键值,size
是共享内存段的大小,shmflg
是创建或访问标志。 - 然后,使用
shmat
函数将共享内存段连接到进程的地址空间。shmat
函数的原型为void *shmat(int shmid, const void *shmaddr, int shmflg);
,其中shmid
是共享内存段标识符,shmaddr
是指定连接的地址(通常为NULL
,让系统自动选择地址),shmflg
是连接标志。 - 进程使用完共享内存后,需要使用
shmdt
函数将共享内存段从进程的地址空间分离,shmdt
函数的原型为int shmdt(const void *shmaddr);
,其中shmaddr
是之前shmat
函数返回的地址。最后,使用shmctl
函数来标记共享内存段可以被删除(当所有进程都不再使用时),shmctl
函数的原型为int shmctl(int shmid, int cmd, struct shmid_ds *buf);
,其中cmd
为IPC_RMID
时表示删除共享内存段。
- 在Linux系统中,使用共享内存需要包含
- 示例代码(两个进程通过共享内存通信):
- 基本概念 :
c
// 创建共享内存并写入数据的进程
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#define BUFFER_SIZE 1024
int main() {
int shmid;
key_t key;
char *shared_memory;
// 生成一个唯一的键值
if ((key = ftok(".", 'a')) == -1) {
perror("生成键值失败");
return 1;
}
// 创建共享内存段
if ((shmid = shmget(key, BUFFER_SIZE, 0666 | IPC_CREAT)) == -1) {
perror("获取共享内存段失败");
return 1;
}
// 将共享内存段连接到进程的地址空间
if ((shared_memory = (char *)shmat(shmid, NULL, 0)) == NULL) {
perror("连接共享内存段失败");
return 1;
}
// 写入数据到共享内存
strcpy(shared_memory, "这是通过共享内存写入的数据。");
// 将共享内存段从进程的地址空间分离
if (shmdt(shared_memory) == -1) {
perror("分离共享内存段失败");
return 1;
}
return 0;
}
c
// 从共享内存读取数据的进程
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#define BUFFER_SIZE 1024
int main() {
int shmid;
key_t key;
char *shared_memory;
// 生成一个唯一的键值
if ((key = ftok(".", 'a')) == -1) {
perror("生成键值失败");
return 1;
}
// 获取共享内存段
if ((shmid = shmget(key, BUFFER_SIZE, 0666 | IPC_CREAT)) == -1) {
perror("获取共享内存段失败");
return 1;
}
// 将共享内存段连接到进程的地址空间
if ((shared_memory = (char *)shmat(shmid, NULL, 0)) == NULL) {
perror("连接共享内存段失败");
return 1;
}
// 读取共享内存中的数据
printf("从共享内存读取到的数据:%s\n", shared_memory);
// 将共享内存段从进程的地址空间分离
if (shmdt(shared_memory) == -1) {
perror("分离共享内存段失败");
return 1;
}
// 标记共享内存段可以被删除
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("删除共享内存段标记失败");
return 1;
}
return 0;
}
-
共享内存(Shared Memory)特点(续)
- 需要使用信号量或互斥锁等同步机制来保证数据的一致性。例如,当一个进程正在向共享内存写入数据时,另一个进程不能同时进行写入操作,否则可能会出现数据混乱。并且共享内存的分配和管理相对复杂,需要考虑内存的大小、地址映射等问题。
-
信号量(Semaphore)
- 基本概念 :
- 信号量是一个计数器,用于控制多个进程对共享资源的访问。它可以实现进程间的同步和互斥。信号量的值表示可用资源的数量,当信号量的值大于0时,表示有可用资源;当信号量的值等于0时,表示没有可用资源,此时请求资源的进程会被阻塞。
- 创建和使用方式 :
- 在Linux系统中,使用信号量需要包含
<sys/types.h>
、<sys/ipc.h>
和<sys/sem.h>
头文件。首先,使用semget
函数创建或获取一个信号量集标识符。semget
函数的原型为int semget(key_t key, int nsems, int semflg);
,其中key
是一个键值,nsems
是信号量集中信号量的个数(通常为1),semflg
是创建或访问标志。 - 然后,使用
semctl
函数对信号量进行初始化等操作。semctl
函数的原型为int semctl(int semid, int semnum, int cmd,...);
,其中semid
是信号量集标识符,semnum
是信号量在集合中的编号(对于只有一个信号量的集合,通常为0),cmd
是操作命令,如SETVAL
用于设置信号量的值。 - 进程可以使用
semop
函数来操作信号量(如对信号量进行P操作和V操作)。semop
函数的原型为int semop(int semid, struct sembuf *sops, unsigned nsops);
,其中sops
是一个指向struct sembuf
结构体数组的指针,struct sembuf
结构体包含信号量操作的相关信息,如操作类型(-1
表示P操作,+1
表示V操作)、信号量编号等,nsops
是操作的个数。
- 在Linux系统中,使用信号量需要包含
- 示例代码(使用信号量实现互斥访问共享资源):
- 基本概念 :
c
// 包含必要的头文件
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <unistd.h>
#include <string.h>
#define BUFFER_SIZE 1024
// 定义联合体,用于semctl函数的参数传递
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
// 信号量P操作函数
void semaphore_p(int semid) {
struct sembuf sop;
sop.sem_num = 0;
sop.sem_op = -1;
sop.sem_flg = 0;
if (semop(semid, &sop, 1) == -1) {
perror("P操作失败");
exit(1);
}
}
// 信号量V操作函数
void semaphore_v(int semid) {
struct sembuf sop;
sop.sem_num = 0;
sop.sem_op = 1;
sop.sem_flg = 0;
if (semop(semid, &sop, 1) == -1) {
perror("V操作失败");
exit(1);
}
}
// 主函数
int main() {
int shmid, semid;
key_t key;
char *shared_memory;
union semun arg;
// 生成一个唯一的键值
if ((key = ftok(".", 'a')) == -1) {
perror("生成键值失败");
return 1;
}
// 创建共享内存段
if ((shmid = shmget(key, BUFFER_SIZE, 0666 | IPC_CREAT)) == -1) {
perror("获取共享内存段失败");
return 1;
}
// 将共享内存段连接到进程的地址空间
if ((shared_memory = (char *)shmat(shmid, NULL, 0)) == NULL) {
perror("连接共享内存段失败");
return 1;
}
// 创建信号量
if ((semid = semget(key, 1, 0666 | IPC_CREAT)) == -1) {
perror("获取信号量失败");
return 1;
}
// 初始化信号量的值为1
arg.val = 1;
if (semctl(semid, 0, SETVAL, arg) == -1) {
perror("初始化信号量失败");
return 1;
}
pid_t pid = fork();
if (pid == -1) {
perror("创建子进程失败");
return 1;
} else if (pid == 0) {
// 子进程
semaphore_p(semid);
strcpy(shared_memory, "这是子进程写入共享内存的数据。");
semaphore_v(semid);
// 将共享内存段从进程的地址空间分离
if (shmdt(shared_memory) == -1) {
perror("子进程分离共享内存段失败");
return 1;
}
} else {
// 父进程
semaphore_p(semid);
printf("父进程从共享内存读取到的数据:%s\n", shared_memory);
semaphore_v(semid);
// 将共享内存段从进程的地址空间分离
if (shmdt(shared_memory) == -1) {
perror("父进程分离共享内存段失败");
return 1;
}
// 标记共享内存段可以被删除
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("删除共享内存段标记失败");
return 1;
}
// 标记信号量可以被删除
if (semctl(semid, 0, IPC_RMID, NULL) == -1) {
perror("删除信号量标记失败");
return 1;
}
}
return 0;
}
- **特点**:
- 信号量主要用于进程间的同步和互斥,能够有效地控制对共享资源的访问。通过合理设置信号量的初始值和操作方式,可以实现复杂的进程同步场景。但是信号量的使用比较复杂,需要正确理解P操作(等待资源)和V操作(释放资源)的含义以及它们之间的关系。如果使用不当,可能会导致死锁等问题。
- 套接字(Socket)
- 基本概念 :
- 套接字是一种更为通用的进程间通信方式,它不仅可以用于同一台计算机上的进程通信,还可以用于不同计算机之间(通过网络)的进程通信。套接字提供了一种基于网络协议(如TCP/IP)的通信接口,使得进程可以像读写文件一样进行网络通信。
- 基本概念 :
- **创建和使用方式**:
- 在Linux系统中,使用套接字需要包含`<sys/types.h>`、`<sys/socket.h>`头文件。首先,使用`socket`函数创建一个套接字。`socket`函数的原型为`int socket(int domain, int type, int protocol);`,其中`domain`表示套接字使用的协议族(如`AF_INET`表示IPv4协议族),`type`表示套接字类型(如`SOCK_STREAM`表示面向连接的TCP套接字,`SOCK_DGRAM`表示无连接的UDP套接字),`protocol`表示协议(通常为0,表示使用默认协议)。
- 对于基于TCP的套接字通信,服务器端需要使用`bind`函数将套接字绑定到一个本地地址和端口,`bind`函数的原型为`int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);`,其中`sockfd`是套接字描述符,`addr`是指向`sockaddr`结构体(或其变体,如`sockaddr_in`用于IPv4地址)的指针,`addrlen`是地址结构体的长度。
- 然后,服务器端使用`listen`函数监听端口,等待客户端连接。`listen`函数的原型为`int listen(int sockfd, int backlog);`,其中`backlog`表示等待连接队列的最大长度。
- 客户端使用`connect`函数连接到服务器。`connect`函数的原型为`int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);`,其中参数含义与`bind`函数类似。
- 服务器端接受客户端连接使用`accept`函数,`accept`函数的原型为`int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);`,它会返回一个新的套接字描述符,用于与客户端进行通信。
- 通信过程中,使用`send`(用于TCP套接字)或`sendto`(用于UDP套接字)函数发送数据,使用`recv`(用于TCP套接字)或`recvfrom`(用于UDP套接字)函数接收数据。
- **示例代码(简单的TCP套接字通信示例,服务器端和客户端)**:
- **服务器端代码**:
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#define PORT 8888
#define BUFFER_SIZE 1024
int main() {
int server_socket, client_socket;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len;
char buffer[BUFFER_SIZE];
// 创建套接字
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == -1) {
perror("创建服务器套接字失败");
return 1;
}
// 初始化服务器地址结构体
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;
// 绑定套接字到本地地址和端口
if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("绑定服务器套接字失败");
close(server_socket);
return 1;
}
// 监听端口
if (listen(server_socket, 5) == -1) {
perror("监听端口失败");
close(server_socket);
return 1;
}
printf("服务器正在等待客户端连接...\n");
// 接受客户端连接
client_addr_len = sizeof(client_addr);
client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_socket == -1) {
perror("接受客户端连接失败");
close(server_socket);
return 1;
}
printf("客户端已连接。\n");
// 接收客户端发送的数据
ssize_t num_read = recv(client_socket, buffer, sizeof(buffer), 0);
if (num_read == -1) {
perror("接收客户端数据失败");
close(client_socket);
close(server_socket);
return 1;
} else if (num_read == 0) {
printf("客户端已断开连接。\n");
} else {
buffer[num_read] = '\0';
printf("接收到客户端发送的数据:%s\n", buffer);
}
// 发送响应数据给客户端
char response[] = "这是服务器的响应数据。";
ssize_t num_written = send(client_socket, response, strlen(response), 0);
if (num_written == -1) {
perror("发送响应数据给客户端失败");
close(client_socket);
close(server_socket);
return 1;
}
// 关闭套接字
close(client_socket);
close(server_socket);
return 0;
}
- **客户端代码**:
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#define PORT 8888
#define BUFFER_SIZE 1024
int main() {
int client_socket;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
// 创建套接字
client_socket = socket(AF_INET, SOCK_STREAM, 0);
if (client_socket == -1) {
perror("创建客户端套接字失败");
return 1;
}
// 初始化服务器地址结构体
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// 连接服务器
if (connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("连接服务器失败");
close(client_socket);
return 1;
}
// 发送数据给服务器
char data[] = "这是客户端发送的数据。";
ssize_t num_written = send(client_socket, data, strlen(data), 0);
if (num_written == -1) {
perror("发送数据给服务器失败");
close(client_socket);
return 1;
}
// 接收服务器响应的数据
ssize_t num_read = recv(client_socket, buffer, sizeof(buffer), 0);
if (num_read == -1) {
perror("接收服务器响应数据失败");
close(client_socket);
return 1;
} else if (num_read == 0) {
printf("服务器已断开连接。\n");
} else {
buffer[num_read] = '\0';
printf("接收到服务器响应的数据:%s\n", buffer);
}
// 关闭套接字
close(client_socket);
return 0;
}
- **特点**:
- 套接字通信功能强大,应用范围广泛,可以实现本地进程间通信和网络进程间通信。但套接字编程相对复杂,需要了解网络协议、IP地址、端口等知识。并且在网络通信中,还需要考虑网络延迟、丢包、字节序等问题。