计算机网络 lab3 使用 Socket 完成多人聊天室 -- 原理

一、服务器和客户端的功能

1. 客户端需要支持下面几个功能

(1) 连接服务器
(2) 支持用户输入聊天消息,发送消息给服务器
(3) 接收并显示服务器的消息
(4) 退出连接

针对上述需求,客户端的实现需要两个进程分别支持下面的功能:
(1) 子进程的功能:
等待用户输入聊天信息
将聊天信息写到管道(pipe),并发送给父进程
(2) 父进程的功能:
使用 epoll 机制接受服务端发来的信息,并显示给用户,使用户看到其他用户的聊天信息
将子进程发给的聊天信息从管道(pipe)中读取,并发送给服务端

2. 服务器端需要支持下面几个功能

(1) 支持多个客户端接入,实现聊天室基本功能
(2) 启动服务建立监听端口等待客户端连接
(3) 使用 epoll 机制实现并发,增加效率
(4) 客户端连接时发送欢迎消息并存储连接记录
(5) 客户端发送消息时广播给其他所有客户端
(6) 客户端请求退出时对连接信息进行清理

3. 模型

二、概念介绍

1. 阻塞与非阻塞 socket

非阻塞设置方式示例代码:

1
2
// 将文件描述符设置为非阻塞方式(利用 fcntl 函数)
fcntl (sockfd, F_SETFL, fcntl (sockfd, F_GETFD, 0)| O_NONBLOCK);

这个函数接受三个参数,第一个参数是文件描述符(linux 认为所有东西都是文件,socket 也是文件),第二个是要对第一个文件做什么动作(设置参数、读取参数之类的),第三个参数设置具体的行为(设置某种参数)。

F_SETFL 从字面意思就是 设置文件的标志 ,F_GETFD 从字面意思就是 读取文件的标志 。

先用 fcntl (sockfd, F_GETFD, 0) 读取套接字信息,读取的是一个整数。任何一个整数都可以用二进制表示,二进制的每一位为 0 或者 1 都可以表示文件的某个属性的状态。

再用 fcntl (sockfd, F_SETFL, fcntl (sockfd, F_GETFD, 0)| O_NONBLOCK); 设置文件的非阻塞属性。| 是 C++ 中的按位或,fcntl (sockfd, F_GETFD, 0)| O_NONBLOCK 的意思就是把表示 sockfd 属性的整数中关于阻塞与非阻塞的位置为有效。最后写入 sockfd 。

(1) STDIN_FILENO 与 STDIN 的区别:

STDIN_FILENO:
(a) 数据类型:int
(b) 层次:系统级的 API,是一个文件句柄,定义在 < unistd.h > 中。
(c) 相应的函数:open (),close (),read (),write (),lseek () 等系统级别的函数。
SDTIN:
(a) 数据类型:FILE *
(b) 层次:c 语言的提供的标准输入流。c 语言标准库封装系统函数实现。高级的输入输出函数。可在 < stdio.h > 中找到外部声明。
(c) 相应的函数:fopen (),fclose (),fread (),fwrite (),fseek () 等 c 语言标准函数。

(2) 文件描述符

文件必须先开启后才能进行读写操作,开启文件后会回传一个 File Descriptor (文件描述器、简称 fd),之后的所有操作都会需要 fd 作为参数。除非每个行程明确将其关闭,否则行程至少会开启 3 个 fd,分别是 stdin (0), stdout (1) 及 stderr (2),实际使用这三个 fd 时不需直接用 0 ~ 2 整数值,unistd.h 有预先定义好的 STDIN_FILENO, STDOUT_FILENO 及 STDERR_FILENO。

  核心内部会替每个行程 (task) 维护一份 file table,放在 current->files,用来记录行程开启的 fd,可以在 linux/sched.h 的 task_struct 中找到他。开档的系统呼叫会调用 fs/open.c 内的 do_sys_open (),里面就会注册 fd 到行程的文件表。

(3) open 函数 - 打开文件

open 函数用来打开一个文件,建立一个文件描述符到文件路径的映射,为后续的各种文件操作建立一个文件标识。
open 函数原型如下:

1
2
#include <fcntl.h>
int open (const char *pathname, int oflag,...);

返回值:如果成功则返回文件描述符,如果出错则返回 - 1。
参数:
(a) pathname 是要打开或创建的文件的路径或者名字。可以是绝对路径,也可以是相对路径。
(b) oflag 是操作选项。是一个整数,每个位代表一个操作选项。通过一些常量的或运算构成综合性的操作参数。
三个常量决定文件的读写模式,可以选择其中一个,不能同时选。这三个是 O_RDONLY、O_WRONLY、O_RDWR。定义在头文件 < fcntl.h > 或者 < bits/fcntl.h > 中。
1
2
3
#define O_RDONLY             00     /* 只读方式打开 */
#define O_WRONLY 01 /* 只写方式打开 */
#define O_RDWR 02 /* 读写方式打开 */

也就是说, oflag 参数的低 2 位 (bit0 和 bit1) 表示文件读写模式, bit0 为 1 表示文件只能用于写操作, bit1 为 1 表示可读也可写, bit0 和 bit1 都为 0 表示只读。

(c) … 说明该参数是可选参数,该参数 mode_t 类型数据。称为文件创建模式字,它决定要的文件的一些权限。参考 umask 函数,这里暂且不管这个参数。

(4) fcntl 函数 cmd 值的 F_GETFL 和 F_SETFL

(a) 获取文件打开方式的标志 (fd 的文件状态标志),标志值含义与 open 调用一致。
三个存取方式标志 (O_RDONLY , O_WRONLY , 以及 O_RDWR) 并不各占 1 位。(这三种标志的值各是 0 , 1 和 2,由于历史原因,这三种值互斥 — 一个文件只能有这三种值之一。) 因此首先必须用屏蔽字 O_ACCMODE 相与取得存取方式位,然后将结果与这三种值相比较。
(b) 将文件描述符 close-on-exec 标志设置为第三个参数 arg 的最后一位
设置给 arg 描述符状态标志,可以更改的几个标志是:O_APPEND,O_NONBLOCK,O_SYNC 和 O_ASYNC。而 fcntl 的文件状态标志总共有 7 个:O_RDONLY , O_WRONLY , O_RDWR , O_APPEND , O_NONBLOCK , O_SYNC 和 O_ASYNC

可更改的几个标志如下面的描述:
O_NONBLOCK 非阻塞 I/O,如果 read (2) 调用没有可读取的数据,或者如果 write (2) 操作将阻塞,则 read 或 write 调用将返回 - 1 和 EAGAIN 错误
O_APPEND 强制每次写 (write) 操作都添加在文件大的末尾,相当于 open (2) 的 O_APPEND 标志
O_DIRECT 最小化或去掉 reading 和 writing 的缓存影响。系统将企图避免缓存你的读或写的数据。如果不能够避免缓存,那么它将最小化已经被缓存了的数据造成的影响。如果这个标志用的不够好,将大大的降低性能
O_ASYNC 当 I/O 可用的时候,允许 SIGIO 信号发送到进程组,例如:当有数据可以读的时候

(5) bind 函数

网络编程 socket 之 bind 函数
socket 绑定的 ip 为 INADDR_ANY 的意义

2.epoll

epoll 是 Linux 内核为处理大批句柄而作改进的 poll,是 Linux 特有的 I/O 函数。
epoll 之所以高效,是因为 epoll 将用户关心的文件描述符放到内核里的一个事件表中,而不是像 select/poll 每次调用都需要重复传入文件描述符集或事件集。比如当一个事件发生(比如说读事件),epoll 无须遍历整个被侦听的描述符集,只要遍历那些被内核 IO 事件异步唤醒而加入就绪队列的描述符集合就行了。

3. 多线程

pthread 简要使用指南(三) 线程的终止

4. 取得本地 IP

linux 的環境下,在 C 程式裡 popen 的用法 & 取得 ip 位置的範例
可以参考这篇文章中在 C 程序中使用 shell 命令的方法
但是我实测它的代码是不能用的,需要进行一些修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void get_local_ip (char *ip)
{
FILE *fpRead;

// 使用 shell command 來取得 ip 值
char* command=(char*)"ifconfig | grep 'inet'| grep -v '127.0.0.1' | cut -d: -f2 | awk '{ print $2}'";
char* renewCh;

fpRead = popen (command, "r");
fgets (ip, IP_SIZE , fpRead);

// 記得作 pclose () 的動作
if (fpRead != NULL)
pclose (fpRead);

// 最後檢查取出的字串當中是否有多餘的換行,若有直接取代為 '\0' 作結尾
renewCh=strstr (ip,"\n");
if (renewCh)
*renewCh= '\0';

//printf ("=== % s ===\n", ip);
}
命令解释如下:
grep ‘inet’ 截取包含 ip 的那两行
grep -v ‘127.0.0.1’ 去掉本地指向的那行
cut -d: -f2 -d: 以:分割字符串 -f2:取第二组数据
awk ‘{print $2}’ $2 表示默认以空格分割的第二组 同理 $1 表示第一组

三、错误

ryu-manager 报错 SyntaxError: invalid syntax

https://blog.csdn.net/jiao424525707/article/details/103045159
https://blog.csdn.net/cyz14/article/details/79994548
但是后来没有解决,因为可以不用到 ryu

Exception: Please shut down the controller which is running on port 6653:

make: * 没有指明目标并且找不到 makefile。 停止。

一开始把文件名取出 Makefile 出错,后来改成 makefile 就可以了

‘iostream’: No such file or directory

先注意你的编译器要用 C++ 编译器,还有副档名(扩展名)要用 “.cpp” 而不能用 “.c”。还不行那就用老式表头档,格式为:#include // 就不用 using namespace std; 这一行了。

Virtualbox 和主机共享粘贴板

Virtualbox 和主机共享粘贴板

补充

命令参数的三大风格:Posix、BSD、GNU
AF_INET 和 PF_INET 的细微不同

gcc 和 g++ 的差别

1
g++ -o staic_test.o static_test.cpp
1
gcc -o staic_test.o static_test.cpp -lstdc++

可正常编译通过。
分析:
gcc 命令不能自动和 C++程序使用的库联接,所以使用 gcc 编译 c++ 代码时,通常需要增加 - lstdc++ 选项,或者直接使用 g++ 来完成联接。g++ 在编译阶段,会自动调用 gcc。

四、参考

mininet 實戰
【Mininet 从入门到精通】第 2 讲 Mininet 网络从创建到部署
SDN 为 host 赋予服务器、客户端的作用
mininet 命令
mininet 常见用法
计算机网络实验二:UDP 套接字编程实现多人聊天 Java 实现
做好这些项目,你就真正的入门 C/C++ 了!
重点 socket 多人聊天程序 C 语言版 (一)
重点 Linux-C 使用 UDP 实现的局域网聊天小程序
重点 C++ 实现即时通信软件
C 语言练手项目 —C 语言编写聊天室
國立台灣大學電機工程學系 網路與多媒體實驗
逸中軟體

计算机网络系列

计算机网络 lab1
计算机网络 lab2 使用 Mininet 创建网络拓扑
计算机网络 lab3 使用 Socket 完成多人聊天室 — 原理
计算机网络 lab3 使用 Socket 完成多人聊天室 — 实作
计算机网络 lab4 Overlay Network 和 VXLAN