1 计算机网络基础
1.1 网络分层模型和代表协议
常见网络分层模型主要有两种:OSI 七层模型和TCP/IP四层模型。
OSI七层模型是ISO组织提出的,如下
- 应用层:HTTP,FTP
- 表示层:ASCII,PICT,TIFF,JPEG
- 会话层:RPC,SQL,NFS
- 传输层:TCP,UDP,SPX
- 网络层:IP,ICMP
- 数据链路层:802.2等等
- 物理层
实际中常常以TCP/IP四层模型代替,其代表协议如下
- 应用层:
http
,ftp
,nfs
,ssh
,telnet
- 传输层:
TCP
、UDP
- 网络层:
IP
,ICMP
,IGMP
- 链路层:以太网帧协议,
ARP
1.2 C/S模型和B/S模型
C/S模型即client/server模型,分为客户端和服务端。
- 优点:客户端可以缓存大量数据,在实现上协议选择灵活,完全可以自己设计协议,速度快
- 缺点:安全性不佳,跨平台性差,开发工作量大
B/S模型即browser/server模型,只需在浏览器中即可访问服务
- 优点:能够跨平台使用,开发工作量小
- 缺点:不能缓存大量数据,需要严格遵守HTTP相关协议
1.3 数据传输过程
应用数据在网络进行传输前需要进行逐层封装,不封装则无法在网络中进行传输。
如TCP/IP协议传输的话,应用数据进入传输层时需要打上TCP首部称为数据段Segment,进入网络层时再打上IP首部称为数据包Packet,最后在链路层中还要打上以太网首部和以太网尾部称为帧Freame。其中TCP首部20Bytes,IP首部20Bytes,以太网首部20Bytes,以太网尾部4Bytes。802.3以太网帧最大为1530Bytes(帧间距另外还需要12Bytes)。
同样接收方在接受到数据的过程中也需要逐层解封装。
1.4 以太网帧格式和ARP协议
ARP(Addree Resolution Protocal)即地址解析协议,用于实现从IP地址到MAC地址的映射,即询问目标IP对应的MAC地址。
在传输数据的封装过程中,不仅需要封装源主机和目的主机的IP地址,还需要封装各自的MAC地址。在上层程序中一般更关心IP地址,所以在传输时,需要将其给出的IP地址通过ARP协议来获取目的主机的MAC地址,完成数据封装。
在以太网链路上传输的数据包称为以太帧。
以太帧起始部分由前导码和帧开始符组成。后面紧跟着一个以太网报头,以MAC地址说明目的地址和源地址。帧的中部是该帧负载的包含其他协议报头的数据包(例如IP协议)。以太帧由一个32位冗余校验码结尾。它用于检验数据传输是否出现损坏。
1.5 IP协议
IP协议是TCP/IP协议族的核心协议,也是socket
网络编程的基础。
IP协议的主要的主要特点如下
- 无状态:IP通信双方不同步传输数据的状态信息,所有IP数据报的发送、传输、接受都是相互独立、没有上下文关系的。这种服务优点在于简单、高效。最大的缺点是无法处理乱序和重复的IP数据报,确保IP数据报完整的工作只能交给上层协议来完成。
- 无连接: IP通信双方都不长久地维持对方的任何信息。上层协议每次发送数据的时候,都需要明确指出对方的IP地址。
- 不可靠: IP协议不能保证IP数据报准确到达接收端,它指承诺尽最大努力交付。IP模块一旦检测到数据报发送失败,就通知上层协议,而不会试图重传。
IPv4的头部见下图
IPv6的头部见下图
注意IP地址为4字节32位。
1.6 TCP和UDP
TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接。 建立连接需要所谓的“三次握手”,断开连接需要“四次挥手”。其对系统资源要求较多,流模式,TCP保证传输数据的有序和正确性。
其包头结构如下图
UDP(User Data Protocol,用户数据报协议)是一个非连接的协议,传输数据之前源端和终端不建立连接, 当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。 在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、 计算机的能力和传输带宽的限制; 在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
注意端口号为2字节8位,所以端口号最大为2^16 - 1 = 65536
,最小为0,端口号小于256的定义为常用端口,服务器一般都是通过常用端口号来识别的。任何TCP/IP实现所提供的服务都用1—1023之间的端口号,是由ICANN来管理的;端口号从1024—49151是被注册的端口,也成为“用户端口”,被IANA指定为特殊服务使用。
大多数TCP/IP实现给临时端口号分配1024—5000之间的端口号。大于5000的端口号是为其他服务器预留的。
关于“三次握手”和“四次挥手”,可参见这里。
2 socket编程相关函数
2.1 字节序转换
字节序又分为大端字节序和小端字节序。
- 大端字节序:将高位数据存放到低位内存地址
- 小端字节序:将低位数据存放到高位内存地址
网络字节序都是大端字节序。
字节序转换的相关函数:
uint32_t htonl(uint32_t hostlong)
:将long
类型的主机字节序转为网络字节序uint16_t htons(uint16_t hostshort)
:将short
类型的主机字节序转为网络字节序uint32_t ntohl(uint32_t netlong)
:同上相反uint16_t ntohs(uint16_t netshort)
:同上相反
2.2 地址转换函数
1 |
|
2.3 地址结构
套接口地址结构
1 | struct socketaddr_in { |
由于历史原因,有些函数需要使用通用地址结构作为参数,所以有时需要将两种结构的指针进行转换
1 | struct sockaddr { |
2.4 相关函数
2.4.1 socket 函数
创建一个套接字用于通信
1 | <sys/socket.h> |
若创建成功返回非负整数,与文件描述符类似,俗称套接口描述字。
失败返回-1。
2.4.2 bind 函数
绑定一个本地地址到套接字
1 | <sys/socket.h> |
三个参数分别为套接字、要绑定的地址、地址长度。
失败返回-1,成功返回0。
2.4.3 listen 函数
将套接字用于监听进入的连接
1 |
|
sockfd
为要监听的套接字,backlog
规定内核为此套接字排队的最大连接个数。
成功返回0,失败返回-1。
监听以后,套接字变为被动套接字,用于在accept
函数之前调用,否则默认为主动套接字。
对于给定的监听套接口,内核要维护两个队列:
- 已有客户发出并到达服务器,服务器正在等待完成相应的TCP三路握手过程
- 已完成连接的队列
2.4.4 accept 函数
从已完成连接队列返回第一个连接,如果已完成连接队列为空则阻塞
1 | include <sys/socket.h> |
sockfd
为服务器套接字,addr
是将返回对等方的套接字地址,addrlen
返回对等方的套接字地址长度。
成功返回非负整数,失败返回-1
2.4.5 connect 函数
功能是建立一个连接至addr
所指定的套接字
1 |
|
sockfd
是未连接的套接字,addr
是要连接的套接字地址,addrlen
是第二个参数addr
的长度。
成功返回0,失败返回-1。
3 C/S模型实现
3.1 基本实现思路
这里实现一个简单的C/S模型,分别实现服务端和客户端,完成从客户端发送字符,服务端收到后将字符转换为大写,然后回传到客户端,客户端收到后将其打印。
服务端的实现流程:
- 使用
socket()
创建套接字 - 使用
bind()
绑定服务端地址和端口号 - 使用
listen()
设定套接字监听上限 - 使用
accpet()
阻塞监听客户端的连接 - 使用
read()
从读取字符,用toupper()
将其转为大写,然后用write()
写会客户端 - 当客户端关闭后,使用
close()
关闭套接字
客户端实现则比较简单
- 使用
socket()
创建套接字 - 使用
connect()
与服务器进行连接 - 使用通过
fgets()
从键盘接收输入,然后使用write()
写向服务端,最后用read()
读取服务端数据,将其打印 - 当接收到中断程序信息,则使用
close()
关闭套接字
3.2 C/S模型1.0版
在这个版本里只需根据3.1节的实现流程按部就班调用函数实现即可
3.2.1 服务端实现
直接看服务端代码
1 |
|
3.2.2 客户端实现
再看客户端代码
1 |
|
其中头文件helper.h
有一些宏和错误辅助函数的实现,后续一些辅助的函数都会放入这个文件中
1 |
|
将服务端和客户端程序分别编译完成后,先运行服务端,再运行客户端。然后在客户端中输入任意小写英文字符,即可看到屏幕将对应大写字符打印在shell中。
3.3 C/S模型2.0版-函数的异常封装
由于我们使用到的大多数函数都需要保证其异常情况,如判断其调用后返回值是否等于-1
,如果在主程序中挨个对其判断异常,则会大大降低主程序的可读性,程序逻辑不够突出。
3.3.1 常用函数封装
因此可以将这些常用的需要保证异常安全的函数进行简单封装,将出错处理与逻辑分离,提高主程序的代码整洁性,如
1 | void sys_err(const char* str) { |
封装后的函数名即是将原函数的首字母大写,并使其形参与原函数完全一致,这样既不影响对原函数的接口调用,又可以提高程序整洁性,同时也方便直接使用man
命名查看相关函数原型。
其他函数的封装过程如下
1 | int Bind(int fd, struct sockaddr* addr, int len) { |
3.3.2 服务端2.0版本
1 |
|
3.3.3 客户端2.0版本
1 |
|
使用效果同1.0版本一致
3.4 C/S模型3.0-多进程并发
前两个版本实现,服务器同时都只能接受一个客户端的连接,这显然并不合理,因此可以使用多进程或者多线程来实现简单的并发功能,使得服务器可以同时接受多个客户端的连接,并正常提供服务。
在此实现一下多进程版本,主要使用fork()
函数来生成子进程。
3.4.1 使用fork()
开启多进程
fork()
函数如下。
1 |
|
fork()
可以讲运行着的程序分成2个几乎完全一样的进程,每个进程都启动一个从代码同一位置开始执行的现成,这两个进程中的线程继续执行。在父进程中,fork()
的返回值是子进程的进程ID;在子进程中fork()
返回值为0;出错时返回 -1。
另外,还需要借助signal
机制来回收子进程,否则进程不安全。
3.4.2 服务端3.0版本
1 |
|
客户端则无需变化。
4 结束语
本文先在第一部分记录了一些 Linux 网络编程中的一些基础知识,然后在第二部分介绍了一个简单 C/S模型中会使用的相关函数,最后在第三部分循序渐进地实现了多个版本简单 C/S 模型,作为练习。
总体来说是网络编程的一个简单入门demo,接下来会对学习到的更深的知识进行记录和总结。