共享内存

概念解释

共享内存允许两个或多个进程共享一定的存储区。就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。因为数据不需要在客户机和服务器端之间复制,数据直接写到内存,不用若干次数据拷贝,所以这是最快的一种IPC

注:共享内存没有任何的同步与互斥机制,所以要使用信号量来实现对共享内存的存取的同步img

共享内存的优势

两个进程地址通过页表映射到同一片物理地址以便于通信,可以给一个区域里面写入数据,也可以从中拿取数据,这也就构成了进程间的双向通信,而且共享内存是IPC通信当中传输速度最快的通信方式没有之一,理由很简单,客户进程和服务进程传递的数据直接从内存里存取、放入,数据不需要在两进程间复制,没有什么操作比这简单了。再者用共享内存进行数据通信,它对数据也没啥限制。

最后就是共享内存的生命周期随内核。即所有访问共享内存区域对象的进程都已经正常结束,共享内存区域对象仍然在内核中存在(除非显式删除共享内存区域对象),在内核重新引导之前,对该共享内存区域对象的任何改写操作都将一直保留;简单地说,共享内存区域对象的生命周期跟系统内核的生命周期是一致的,而且共享内存区域对象的作用域范围就是在整个系统内核的生命周期之内。

共享内存的缺点

由于共享内存并未提供同步机制,在一个服务进程结束对共享内存的写操作之前,并没有自动机制可以阻止另一个进程(客户进程)开始对它进行读取。这明显还达不到我们想要的,我们不单是在两进程间交互数据,还想实现多个进程对共享内存的同步访问,这也正是使用共享内存的窍门所在。基于此,我们通常会用平时常谈到和用到 信号量来实现对共享内存同步访问控制。

共享内存的头文件及相关函数

头文件

1
2
3
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

创建共享内存的函数

1
2
int shmget(key_t key, size_t size, int shmflg);
//成功返回共享内存的ID,出错返回-1
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
(1)第一个参数**key是长整型(唯一非零)**,系统建立IPC通讯 ( 消息队列、 信号量和 共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函数得到,由内核变成标识符,要想**让两个进程看到同一个信号集,只需设置key值不变就可以**。

(2)第二个参数**size指定共享内存的大小**,它的值一般为一页大小的整数倍(未到一页,操作系统向上对齐到一页,但是用户实际能使用只有自己所申请的大小)。(Linux一页通常是4096)

(3)第三个参数**shmflg是一组标志,创建一个新的共享内存,将shmflg 设置了IPC_CREAT标志后,共享内存存在就打开**。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的共享内存,如果共享内存已存在,返回一个错误。一般我们会还或上一个文件权限。例如:( 0666|IPC_CREAT)。

### 挂接函数

创建共享存储段之后,将进程连接到它的地址空间。

```c
void *shmat(int shm_id, const void *shm_addr, int shmflg);
//成功返回指向共享存储段的指针,出错返回-1 
```

(1)第一个参数,shm_id是由shmget函数返回的共享内存标识。

(2)第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。

(3)第三个参数,shm_flg是一组标志位,通常为0

### 分离函数

将进程断开与共享内存的链接,该操作不从系统中删除标识符和其数据结构,要显示调用shmctl(带命令IPC_RMID)才能删除它。

```c
int shmdt(const void *shmaddr);
//成功返回0,出错返回-1
```

(1)addr参数是以前调用shmat时的返回值

## 使用共享内存进行进程间通信

下面就以两个不相关的进程来说明进程间如何通过共享内存来进行通信。其中一个文件shmread.c创建共享内存,并读取其中的信息,另一个文件shmwrite.c向共享内存中写入数据。为了方便操作和数据结构的统一,为这两个文件定义了相同的数据结构,定义在文件shmdata.c中。结构shared_use_st中的written作为一个可读或可写的标志,非0:表示可读,0表示可写,text则是内存中的文件。

### shmdata.h的源代码如下:

```c
#ifndef _SHMDATA_H_HEADER
#define _SHMDATA_H_HEADER
#define TEXT_SZ 2048
struct shared_use_st
{
int written;//作为一个标志,非0:表示可读,0表示可写
char text[TEXT_SZ];//记录写入和读取的文本
};
#endif
```

### 源文件shmread.c的源代码如下:

```c
/*************************************************************************
> File Name: read.c
> Author: echo
> Mail: 12345678@qq.com
> Created Time: 2020年12月19日 星期六 09时37分34秒
************************************************************************/

#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <unistd.h>

#include "shmdata.h"
int main() {
int running = 1; //程序是否继续运行的标志
void *shm = NULL; //分配的共享内存的原始首地址
struct shared_use_st *shared; //指向shm
int shmid; //共享内存标识符 //创建共享内存
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);
if (shmid == -1) {
fprintf(stderr, "shmget failed\n");
exit(EXIT_FAILURE);
} //将共享内存连接到当前进程的地址空间
shm = shmat(shmid, 0, 0);
if (shm == (void *)-1) {
fprintf(stderr, "shmat failed\n");
exit(EXIT_FAILURE);
}
printf("\nMemory attached at %X\n", (int)shm); //设置共享内存
shared = (struct shared_use_st *)shm;
shared->written = 0;
while (running) //读取共享内存中的数据
{ //没有进程向共享内存定数据有数据可读取
if (shared->written != 0) {
printf("You wrote: %s", shared->text);
sleep(rand() % 3); //读取完数据,设置written使共享内存段可写
shared->written = 0; //输入了end,退出循环(程序)
if (strncmp(shared->text, "end", 3) == 0) running = 0;
} else //有其他进程在写数据,不能读取数据
sleep(1);
} //把共享内存从当前进程中分离
if (shmdt(shm) == -1) {
fprintf(stderr, "shmdt failed\n");
exit(EXIT_FAILURE);
} //删除共享内存
if (shmctl(shmid, IPC_RMID, 0) == -1) {
fprintf(stderr, "shmctl(IPC_RMID) failed\n");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}

```

### 源文件shmwrite.c的源代码如下:

```c
/*************************************************************************
> File Name: write.c
> Author: echo
> Mail: 12345678@qq.com
> Created Time: 2020年12月19日 星期六 09时37分26秒
************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#include <unistd.h>

#include "shmdata.h"
int main() {
int running = 1;
void *shm = NULL;
struct shared_use_st *shared = NULL;
char buffer[BUFSIZ + 1]; //用于保存输入的文本
int shmid; //创建共享内存
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);
if (shmid == -1) {
fprintf(stderr, "shmget failed\n");
exit(EXIT_FAILURE);
} //将共享内存连接到当前进程的地址空间
shm = shmat(shmid, (void *)0, 0);
if (shm == (void *)-1) {
fprintf(stderr, "shmat failed\n");
exit(EXIT_FAILURE);
}
printf("Memory attached at %X\n", (int)shm); //设置共享内存
shared = (struct shared_use_st *)shm;
while (running) //向共享内存中写数据
{ //数据还没有被读取,则等待数据被读取,不能向共享内存中写入文本
while (shared->written == 1) {
sleep(1);
printf("Waiting...\n");
} //向共享内存中写入数据
printf("Enter some text: ");
fgets(buffer, BUFSIZ, stdin);
strncpy(shared->text, buffer,
TEXT_SZ); //写完数据,设置written使共享内存段可读
shared->written = 1; //输入了end,退出循环(程序)
if (strncmp(buffer, "end", 3) == 0) running = 0;
} //把共享内存从当前进程中分离
if (shmdt(shm) == -1) {
fprintf(stderr, "shmdt failed\n");
exit(EXIT_FAILURE);
}
sleep(2);
exit(EXIT_SUCCESS);
}:
```

### 结果如下

```shell
china@ubuntu:~/Desktop/codefile/test/signal/sharedMemory$ ./read

Memory attached at E8D0D000
You wrote: 2134
You wrote: hhhh
You wrote: gooodjob
^C
```

```shell
china@ubuntu:~/Desktop/codefile/test/signal/sharedMemory$ ./write
Memory attached at 724B3000
Enter some text: 2134
Waiting...
Waiting...
Enter some text: hhhh
Waiting...
Waiting...
Enter some text: gooodjob
Waiting...
Enter some text: ^C
```
# 消息队列

转载来源:简书

作者:小王

链接:https://www.jianshu.com/p/7e3045cf1ab8



## 消息队列简介

消息队列本质上是**位于内核空间的链表,链表的每个节点都是一条消息**。每一条消息都有自己的消息类型,消息类型用整数来表示,而且必须大于 0。**每种类型的消息都被对应的链表所维护**:

![img](https://upload-images.jianshu.io/upload_images/16823531-baf6c22c59b827fa.png?imageMogr2/auto-orient/strip|imageView2/2/w/685/format/webp)

其中数字 1 表示类型为 1 的消息,数字2、3、4 类似。彩色块表示消息数据,它们被挂在对应类型的链表上。

值得注意的是,刚刚说过没有消息类型为 0 的消息,实际上,**消息类型为 0 的链表记录了所有消息加入队列的顺序**,其中红色箭头表示消息加入的顺序。

## 消息队列相关的函数

1
2
3
4
5
6
7
8
// 创建和获取 ipc 内核对象
int msgget(key_t key, int flags);
// 将消息发送到消息队列
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// 从消息队列获取消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
// 查看、设置、删除 ipc 内核对象(用法和 shmctl 一样)
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
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
## 消息数据格式

无论是发送还是接收消息,消息的格式都必须按照规范来。简单的说,它一般长成下面这个样子:

```c
struct Msg{
long type; // 消息类型。这个是必须的,而且值必须 > 0,这个值被系统使用
// 消息正文,多少字节随你而定
// ...
};
```

所以,只要你保证首4字节(32 位 linux 下的 long)是一个整数就行了。

```c
struct Msg {
long type;
char name[20];
int age;
} msg;
struct Msg {
long type;
int start;
int end;
} msg;
```

## msgsnd 函数

msgsnd 函数用于将数据发送到消息队列。如果该函数被信号打断,会设置 errno 为 EINTR。

```c
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数 msqid:ipc 内核对象 id
参数 msgp:消息数据地址
参数 msgsz:消息正文部分的大小(不包含消息类型)
参数 msgflg:可选项
该值为 0:如果消息队列空间不够,msgsnd 会阻塞。
IPC_NOWAIT:直接返回,如果空间不够,会设置 errno 为 EAGIN.

返回值:0 表示成功,-1 失败并设置 errno。
```

## msgrcv 函数

msgrcv 函数从消息队列取出消息后,并将其从消息队列里删除。

```c
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数 msqid:ipc 内核对象 id
参数 msgp:用来接收消息数据地址
参数 msgsz:消息正文部分的大小(不包含消息类型)
参数 msgtyp:指定获取哪种类型的消息

msgtyp = 0:获取消息队列中的第一条消息
msgtyp > 0:获取类型为 msgtyp 的第一条消息,除非指定了 msgflg 为MSG_EXCEPT,这表示获取除了 msgtyp 类型以外的第一条消息。
msgtyp < 0:获取类型 ≤|msgtyp|≤|msgtyp| 的第一条消息。
参数 msgflg:可选项。
如果为 0 表示没有消息就阻塞。
IPC_NOWAIT:如果指定类型的消息不存在就立即返回,同时设置 errno 为 ENOMSG
MSG_EXCEPT:仅用于 msgtyp > 0 的情况。表示获取类型不为 msgtyp 的消息
MSG_NOERROR:如果消息数据正文内容大于 msgsz,就将消息数据截断为 msgsz
```

## 实例

程序 msg_send 和 msg_recv 分别用于向消息队列发送数据和接收数据。

### msg_send

msg_send 程序定义了一个结构体 Msg,消息正文部分是结构体 Person。该程序向消息队列发送了 10 条消息。

```c
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct {
char name[20];
int age;
}Person;

typedef struct {
long type;
Person person;
}Msg;

int main(int argc, char *argv) {
int id = msgget(0x8888, IPC_CREAT | 0664);

Msg msg[10] = {
{1, {"Luffy", 17}},
{1, {"Zoro", 19}},
{2, {"Nami", 18}},
{2, {"Usopo", 17}},
{1, {"Sanji", 19}},
{3, {"Chopper", 15}},
{4, {"Robin", 28}},
{4, {"Franky", 34}},
{5, {"Brook", 88}},
{6, {"Sunny", 2}}
};

int i;
for (i = 0; i < 10; ++i) {
int res = msgsnd(id, &msg[i], sizeof(Person), 0);
}

return 0;
}
```

程序 msg_send 第一次运行完后,内核中的消息队列大概像下面这样:

![img](https://upload-images.jianshu.io/upload_images/16823531-f02e045b498ff1c0.png?imageMogr2/auto-orient/strip|imageView2/2/w/592/format/webp)

### msg_recv

msg_recv 程序接收一个参数,表示接收哪种类型的消息。比如./msg_recv 4 表示接收类型为 4 的消息,并打印在屏幕。

```c
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

typedef struct {
char name[20];
int age;
}Person;

typedef struct {
long type;
Person person;
}Msg;

void printMsg(Msg *msg) {
printf("{ type = %ld, name = %s, age = %d }\n",
msg->type, msg->person.name, msg->person.age);
}

int main(int argc, char *argv[]) {
if (argc < 2) {
printf("usage: %s <type>\n", argv[0]);
return -1;
}

// 要获取的消息类型
long type = atol(argv[1]);

// 获取 ipc 内核对象 id
int id = msgget(0x8888, 0);

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
Msg msg;
int res;

while(1) {
// 以非阻塞的方式接收类型为 type 的消息
res = msgrcv(id, &msg, sizeof(Person), type, IPC_NOWAIT);
if (res < 0) {
// 如果消息接收完毕就退出,否则报错并退出
if (errno == ENOMSG) {
printf("No message!\n");
break;
}

}
// 打印消息内容
printMsg(&msg);
}
return 0;
}
```

### 子进程与父进程通信

```c
/*************************************************************************
> File Name: msg.c
> Author: echo
> Mail: 12345678@qq.com
> Created Time: 2020年12月19日 星期六 10时31分21秒
************************************************************************/

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <unistd.h>

#include "fcntl.h"
#include "signal.h"
#include "stdio.h"
#include "stdlib.h"
#include "sys/stat.h"
#include "sys/types.h"
#include "sys/wait.h"
#include "unistd.h"

typedef struct {
long type; // Msg hander
char buf[20];
} Msg;

void child() {
long type = atol(" ");
int id = msgget(0x8888, 0);
Msg msg;
while (1) {
int res = msgrcv(id, &msg, 20, type, IPC_NOWAIT);
if (res < 0) {
if (errno == ENOMSG) {
printf("NO message!\n");
break;
}
}
printf("%s\n", msg.buf);
}
}

int main(int argc, char* argv[]) {
// printf("the child\n");
pid_t pid;
int status = 0;
int id = msgget(0x8888, IPC_CREAT | 0666);
Msg msg[5] = {{1, "hello"},
{2, "im echo"},
{3, "my addr is 0x1234"},
{4, "good"},
{5, "bye"}};
int i = 0;
for (; i < 5; i++) {
int res = msgsnd(id, &msg[i], 20, 0);
}
// printf("the where\n");
pid = fork();
if (pid == 0) {
child();
} else {
waitpid(pid, &status, 0);
printf("the child recv msg!\n");
}
exit(EXIT_SUCCESS);
}
```
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
## msgctl函数

获取和设置消息队列的属性

```c
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msqid:消息队列标识符
cmd:控制指令
IPC_STAT:获得msgid的消息队列头数据到buf中
IPC_SET:设置消息队列的属性,要设置的属性需先存储在buf中,可设置的属性包括:msg_perm.uid、msg_perm.gid、msg_perm.mode以及msg_qbytes
buf:消息队列管理结构体。

返回值:
成功:0
出错:-1,错误原因存于error中
EACCESS:参数cmd为IPC_STAT,确无权限读取该消息队列
EFAULT:参数buf指向无效的内存地址
EIDRM:标识符为msqid的消息队列已被删除
EINVAL:无效的参数cmd或msqid
EPERM:参数cmd为IPC_SET或IPC_RMID,却无足够的权限执行
```

### 实例

```c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <error.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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
struct msgbuf{
long mtype ;
char mtext[] ;
} ;

int main(int argc, char **argv){
int msqid ;
struct msqid_ds info ;
struct msgbuf buf ;
struct msgbuf buf1 ;
int flag ;
int sendlength, recvlength ;

msqid = msgget( IPC_PRIVATE, 0666 ) ;

if ( msqid < 0 ){
perror("get ipc_id error") ;
return -1 ;
}

buf.mtype = 1 ;
strcpy(buf.mtext, "happy new year!") ;
sendlength = sizeof(struct msgbuf) - sizeof(long) ;
flag = msgsnd( msqid, &buf, sendlength , 0 ) ;

if ( flag < 0 ){
perror("send message error") ;
return -1 ;
}

buf.mtype = 3 ;
strcpy(buf.mtext, "good bye!") ;
sendlength = sizeof(struct msgbuf) - sizeof(long) ;
flag = msgsnd( msqid, &buf, sendlength , 0 ) ;

if ( flag < 0 ){
perror("send message error") ;
return -1 ;
}

flag = msgctl( msqid, IPC_STAT, &info ) ;

if ( flag < 0 ){
perror("get message status error") ;
return -1 ;
}

printf("uid:%d, gid = %d, cuid = %d, cgid= %d\n" ,
info.msg_perm.uid, info.msg_perm.gid, info.msg_perm.cuid, info.msg_perm.cgid ) ;

printf("read-write:%03o, cbytes = %lu, qnum = %lu, qbytes= %lu\n" ,
info.msg_perm.mode&0777, info.msg_cbytes, info.msg_qnum, info.msg_qbytes ) ;

system("ipcs -q") ;
recvlength = sizeof(struct msgbuf) - sizeof(long) ;
memset(&buf1, 0x00, sizeof(struct msgbuf)) ;

flag = msgrcv( msqid, &buf1, recvlength ,3,0 ) ;
if ( flag < 0 ){
perror("recv message error") ;
return -1 ;
}
printf("type=%d, message=%s\n", buf1.mtype, buf1.mtext) ;

flag = msgctl( msqid, IPC_RMID,NULL) ;
if ( flag < 0 ){
perror("rm message queue error") ;
return -1 ;
}
system("ipcs -q") ;
return 0 ;
}
```

成功为非负描述符,若出错则为-1
```

套接字socket

什么是网络通信:

同一主机上,不同进程可用进程号(process ID)唯一标识。但在网络环境下,各主机独立分配的进程号不能唯一标识该进程。例如,主机A赋于某进程号5,在B机中也可以存在5号进程,因此,“5号进程”这句话就没有意义了。 其次,操作系统支持的网络协议众多,不同协议的工作方式不同,地址格式也不同。因此,网间进程通信还要解决多重协议的识别问题。
其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,因此,万物皆为socket。

什么是TCP/UDP

TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网(WANs)设计的。
TCP/IP协议存在于OS中,网络服务通过OS提供,在OS中增加支持TCP/IP的系统调用——Berkeley套接字,如Socket,Connect,Send,Recv等
UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是属于TCP/IP协议族中的一种。

img

TCP/IP协议族包括运输层、网络层、链路层,而socket所在位置如图,Socket是应用层与TCP/IP协议族通信的中间软件抽象层。

套接字API最初是作为UNIX操作系统的一部分而开发的,所以套接字API与系统的其他I/O设备集成在一起。特别是,当应用程序要为因特网通信而创建一个套接字(socket)时,操作系统就返回一个小整数作为描述符(descriptor)来标识这个套接字。然后,应用程序以该描述符作为传递参数,通过调用函数来完成某种操作(例如通过网络传送数据或接收输入的数据)。
在许多操作系统中,套接字描述符和其他I/O描述符是集成在一起的,所以应用程序可以对文件进行套接字I/O或I/O读/写操作。
当应用程序要创建一个套接字时,操作系统就返回一个小整数作为描述符,应用程序则使用这个描述符来引用该套接字需要I/O请求的应用程序请求操作系统打开一个文件。操作系统就创建一个文件描述符提供给应用程序访问文件。从应用程序的角度看,文件描述符是一个整数,应用程序可以用它来读写文件。下图显示,操作系统如何把文件描述符实现为一个指针数组,这些指针指向内部数据结构

内部的数据结构

对于每个程序系统都有一张单独的表。精确地讲,系统为每个运行的进程维护一张单独的文件描述符表。当进程打开一个文件时,系统把一个指向此文件内部数据结构的指针写入文件描述符表,并把该表的索引值返回给调用者 。应用程序只需记住这个描述符,并在以后操作该文件时使用它。操作系统把该描述符作为索引访问进程描述符表,通过指针找到保存该文件所有的信息的数据结构。

套接字API里有个函数socket,它就是用来创建一个套接字。套接字设计的总体思路是,单个系统调用就可以创建任何套接字,因为套接字是相当笼统的。一旦套接字创建后,应用程序还需要调用其他函数来指定具体细节。例如调用socket将创建一个新的描述符条目:

虽然套接字的内部数据结构包含很多字段,但是系统创建套接字后,大多数字字段没有填写。应用程序创建套接字后在该套接字可以使用之前,必须调用其他的过程来填充这些字段。

3、文件描述符和文件指针的区别:

文件描述符:在linux系统中打开文件就会获得文件描述符,它是个很小的正整数。每个进程在PCB(Process Control Block)中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针。
文件指针:C语言中使用文件指针做为I/O的句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构包括一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引,因此从某种意义上说文件指针就是句柄的句柄(在Windows系统上,文件描述符被称作文件句柄)。

函数用法

socket函数

1
2
3
4
\#include <sys/socket.h>
socket(int *family*,int *type*, int *protocol*)
参数:*family* 指明协议族,*type* 指数据类型,*protocol* 指协议
返回值:

img

imgimg

bind函数

1
2
3
4
5
#include <sys/socket.h>
int bind(int sockfd,const struct sockaddr * myaddr, socklen_t addrlen)
参数:sockfd指socket套接字描述符,myaddr指向需要绑定的网络地址结构,addrlen是网络地址结构的长度
返回值:成功返回0,出错返回-1

listen函数

1
2
3
4
5
#include <sys/socket.h>
int listen(int sockfd,int backlog)
输入参数:sockfd是socket套接字描述符,backlog可以理解为连接队列容量大小(已连接+未连接)
返回值:成功返回0,出错返回-1

accept函数

1
2
3
4
5
#include <sys/socket.h>
int accept(int sockfd, const struct sockaddr * clientaddr, socklen_t addrlen)
输入参数:sockfd是socket套接字描述符,clientaddr指向客户的网络地址结构,addrlen是网络地址结构的大小。
返回值:成功返回非负描述符,出错返回-1

connect函数

1
2
3
4
5
6
7
8
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr * servaddr, socklen_t addrlen)
TCP客户端用connect函数发起与服务器的连接
参数:sockfd指socket函数返回的套接字描述符,*servaddr是指向套接字结构的指针,addrlen是套接字结构的大小
返回值:成功返回0,出错返回-1
sockfd是客户端套接字描述符
servaddr套接字结构必须含有服务器IP和端口

send函数

1
2
3
4
ssize_t send(int sockfd, const char *buf, size_t len, int flags );
参数:sockfd套接字(已连接套接字),buf是发送缓存区,len是缓存区长度,
返回值:copy的字节数

recv函数

1
2
3
ssize_t recv(int sockfd, char *buf, size_t len, int flags)
参数:参数:sockfd套接字(已连接套接字),buf是接收缓存区,len是缓存区长度。
返回值:copy的字节数

close函数

1
2
close(int sockfd)将套接字标记为关闭
参数:sockfd指socket函数返回的套接字描述符

read函数

1
2
3
4
5
#include<unistd.h>
sszie_t read(int fd, void* buf, size_t nbyte)
参数:fd文件描述符,nbyte
返回值:读到的字节数;若到文件尾部,返回0;若出错返回-1

write函数

1
2
3
4
#include<unistd.h>
sszie_t write(int fd, void* buf, size_t nbyte)
参数:fd文件描述符,buf待写入的缓存
返回值:若成功,返回写入的字节数;若出错返回-1

socket(), connect(), accept()三个函数之间的关系

socket函数创建的监听套接字(listening socket)描述符,随后用作bind和listen的第一个参数

accept函数返回值为已连接套接字(connected socket)描述符

一个服务器通常仅仅创建一个监听套接字,在服务器生命周期中一直存在;

内核为每个已连接客户端创建一个已连接套接字

服务端处理流程:

调用socket函数,建立套接字描述符

创建网络地址数据结构,指定要监听的IP和PORT

调用bind函数,将套接字描述符合万罗地址数据结构绑定

调用listen函数,将套接字描述符转为监听套接字,表示该描述符是用于从指定地址和端口接收连接的

阻塞等待,调用accept函数来获取连接

得到连接后使用read和write函数往描述符中读写数据

完成后调用close关闭套接字描述符

客户端处理流程

调用socket函数,创建套接字描述符

创建网络地址结构,指定要连接的服务端IP和PORT

调用connect函数连接服务器

连接成功后调用read和write函数读写数据

完成后调用close关闭描述符

img

示例代码

服务端

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
/* File Name: server.c */  
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define DEFAULT_PORT 8000
#define MAXLINE 4096
int main(int argc, char** argv)
{
int socket_fd, connect_fd;
struct sockaddr_in servaddr;
char buff[4096];
int n;
//初始化Socket
if( (socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){
printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);
exit(0);
}
//初始化
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址。
servaddr.sin_port = htons(DEFAULT_PORT);//设置的端口为DEFAULT_PORT

//将本地地址绑定到所创建的套接字上
if( bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
exit(0);
}
//开始监听是否有客户端连接
if( listen(socket_fd, 10) == -1){
printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
exit(0);
}
printf("======waiting for client's request======\n");
while(1){
//阻塞直到有客户端连接,不然多浪费CPU资源。
if( (connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1){
printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
continue;
}
//接受客户端传过来的数据
n = recv(connect_fd, buff, MAXLINE, 0);
//向客户端发送回应数据
if(!fork()){ /*紫禁城*/
if(send(connect_fd, "Hello,you are connected!\n", 26,0) == -1)
perror("send error");
close(connect_fd);
exit(0);
}
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
close(connect_fd);
}
close(socket_fd);
}
```
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
### 客户端

```c
/* File Name: client.c */

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>

#define MAXLINE 4096
int main(int argc, char** argv)
{
int sockfd, n,rec_len;
char recvline[4096], sendline[4096];
char buf[MAXLINE];
struct sockaddr_in servaddr;
if( argc != 2){
printf("usage: ./client <ipaddress>\n");
exit(0);
}
if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
exit(0);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8000);
if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){
printf("inet_pton error for %s\n",argv[1]);
exit(0);
}
if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
exit(0);
}
printf("send msg to server: \n");
fgets(sendline, 4096, stdin);
if( send(sockfd, sendline, strlen(sendline), 0) < 0)
{
printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
if((rec_len = recv(sockfd, buf, MAXLINE,0)) == -1) {
perror("recv error");
exit(1);
}
buf[rec_len] = '\0';
printf("Received : %s ",buf);
close(sockfd);
exit(0);
}