fomo3d网站开发,做网站补贴,企业为什么要ipo,模版建站网络基础概念和 socket 编程 学习目标#xff1a; 了解 OSI 七层模型、TCP/IP 四层模型结构了解常见的网络协议格式掌握网络字节序和主机字节序之间的转换理解 TCP 服务器端通信流程理解 TCP 客户端通信流程实现 TCP 服务器端和客户端的代码 推荐一个非常好的学习资料仓库
协…网络基础概念和 socket 编程 学习目标 了解 OSI 七层模型、TCP/IP 四层模型结构了解常见的网络协议格式掌握网络字节序和主机字节序之间的转换理解 TCP 服务器端通信流程理解 TCP 客户端通信流程实现 TCP 服务器端和客户端的代码 推荐一个非常好的学习资料仓库
协议
协议的概念
协议是事先约定好大家共同遵守的一组规则如交通信号灯。从应用的角度出发协议可理解为“规则”是数据传输和数据的解释的规则可以简单的理解为各个主机之间进行通信所使用的共同语言。
假设主机 A、主机 B 双方欲传输文件规定
第一次传输文件名接收方接收到文件名应答 OK 给传输方第二次发送文件的尺寸接收方接收到该数据再次应答一个 OK第三次传输文件内容接收方接收数据完成后应答 OK 表示文件内容接收成功
由此无论 A、B 之间传递何种文件都是通过三次数据传输来完成。A、B 之间形成了一个最简单的数据传输规则。双方都按此规则发送、接收数据。A、B 之间达成的这个相互遵守的规则即为协议。
这种仅在 A、B 之间被遵守的协议称之为原始协议。当此协议被更多的人采用不断的增加、改进、维护、完善。最终形成一个稳定的、完整的文件传输协议被广泛应用于各种文件传输过程中。该协议就成为一个标准协议。最早的 ftp 协议就是由此衍生而来。
典型的协议
实际生活中有以下几种常见的协议
应用层常见的协议有 HTTP 协议、FTP 协议等 HTTP超文本传输协议(Hyper Text Transfer Protocol)是互联网上应用最广泛的一种网络协议FTP文件传输协议(File Transfer Protocol) 传输层常见的协议有 TCP/UDP 协议 TCP传输控制协议(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议UDP用户数据协议(User Datagram Protocol)是 OSI 参考模型中一种无连接的传输层协议提供面向事务的简单不可靠信息传送服务 网络层常见的协议有 IP 协议、ICMP 协议、IGMP 协议 IP因特网互联协议(Internet Protocol)ICMPInternet 控制报文协议(Internet Control Message Protocol)是 TCP/IP 协议族的一个子协议用于在 IP 主机、路由器之间传递控制消息IGMPInternet 组管理协议(Internet Group Management Protocol)是因特网协议家族中的一个组播协议该协议运行在主机和组播路由器之间 网络接口层常见的协议有 ARP 协议、RARP 协议 ARP正向地址解析协议(Address Resolution Protocol)通过已知的 IP寻找对应主机的 MAC 地址RARP反向地址转换协议通过 MAC 地址确定 IP 地址
网络模型
OSI 七层模型
OSI 七层模型是国际标准组织制定的 OSI 理论模型该模型定义了不同计算机互联的标准, 是设计和描述计算机网络通信的基本框架。七层模型分别是以下几个(从上向下)
应用层是最靠近用户的 OSI 层这一层为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务表示层可确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。例如PC 程序与另一台计算机进行通信其中一台计算机使用扩展二一十进制交换码(EBCDIC)而另一台则使用美国信息交换标准码(ASCII)来表示相同的字符。如有必要表示层会通过使用一种通格式来实现多种数据格式之间的转换会话层通过传输层(端口号传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或者接受会话请求(设备之间需要互相认识可以是IP也可以是MAC或者是主机名)传输层定义了一些传输数据的协议和端口号(WWW 端口 80 等)主要是将从下层接收的数据进行分段和传输到达目的地址后再进行重组常常把这一层数据叫做段网络层在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择。Internet 的发展使得从世界各站点访问信息的用户数大大增加而网络层正是管理这种连接的层数据链路层定义了如何让格式化数据以帧为单位进行传输以及如何让控制对物理介质的访问。这一层通常还提供错误检测和纠正以确保数据的可靠传输。如串口通信中使用到的 115200、8、N、1物理层主要定义物理设备标准如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由 1、0 转化为电流强弱来进行传输到达目的地后再转化为 1、0也就是我们常说的数模转换与模数转换)这一层的数据叫做比特 TCP/IP 四层模型
在实际生产开发中讨论更多的是 TCP/IP 四层模型这是对七层模型的简化 TCP/IP 网络协议栈分为应用层(Application)、传输层(Transport)、网络层(Network)和链路层(Link)四层如下图所示 数据通信过程
数据的通信过程本质上在发送方是一个层层打包的过程在接收方是一个层层解包的过程这种打包的过程是内核帮我们完成。如下图所示PC 机 A 通过网络向 PC 机 B 发送数据的过程 网络应用程序设计模式
目前使用最多的设计模式就两种 C/S 和 B/S
C/S客户端和服务器模式需要在通讯两端各自部署客户机和服务器来完成数据通信 优点客户端在本机上可以保证性能可以将数据缓存到本地提高数据的传输效率提高用户体验效果客户端和服务端程序都是由同一个开发团队开发协议选择比较灵活缺点服务器和客户端都需要开发工作量相对较大调试困难开发周期长从用户的角度看需要将客户端安装到用户的主机上对用户主机的安全构成威胁 B/S浏览器和服务器模式只需在一端部署服务器而另一端使用浏览器即可完成数据传输 优点无需安装客户端可以使用标注你的浏览器作为客户端只需要开发服务器工作量相对较小由于采用标准的客户端所以移植性好不受平台限制相对安全不用安装软件缺点由于没有客户端数据缓冲不尽人意数据传输有限制用户体验较差通信协议选择只能使用 HTTP 协议协议选择不够灵活
以太网帧
以太网帧格式就是包装在网络接口层(数据链路层)的协议具体格式如下 以 ARP 为例其协议格式具体如下
源 MAC 地址、目的 MAC 地址在以太网首部和 ARP 请求中各出现一次对于链路层为以太网的情况是多余的但如果链路层是其它类型的网络则有可能是必要的。硬件类型指链路层网络类型1 为以太网协议类型指要转换的地址类型0x0800 为 IP 地址后面两个地址长度对于以太网地址和IP地址分别为 6 和 4字节op 字段为 1 表示 ARP 请求op 字段为 2 表示 ARP 应答。
假设现在 PC 机 A 向 PC 机 B 发送请求简单的流程如下
PC 机 A 将本机的 MAC 地址填入源地址以广播的方式发送 ARP 报文因此数据报文的格式为 以太网首部(14字节)0000: ff ff ff ff ff ff 00 05 5d 61 58 a8 08 06 —— 目的主机采用广播地址源主机的 MAC 地址是00:05:5d:61:58:a8上层协议类型 0x0806 表示 ARPARP 帧(28 字节) 0000: 00 01 —— 硬件类型 0x0001 表示以太网0010: 08 00 06 04 00 01 00 05 5d 61 58 a8 c0 a8 00 37 —— 协议类型 0x0800 表示 IP 协议硬件地址(MAC地址)长度为 6协议地址(IP地址)长度为 4op 为 0x0001 表示请求目的主机的 MAC 地址源主机 MAC 地址为 00:05:5d:61:58:a8源主机 IP 地址为 c0 a8 00 37(192.168.0.55)0020: 00 00 00 00 00 00 c0 a8 00 02 —— 目的主机 MAC 地址全 0 待填写目的主机 IP 地址为 c0 a8 00 02(192.168.0.2) 填充位(18 字节)由于以太网规定最小数据长度为46字节ARP帧长度只有28字节因此有18字节填充位填充位的内容没有定义与具体实现相关 0020: 00 77 31 d2 50 100030: fd 78 41 d3 00 00 00 00 00 00 00 00 PC 机 B 收到 ARP 数据报文后发送应答 以太网首部(14字节)0000: 00 05 5d 61 58 a8 00 05 5d a1 b8 40 08 06 —— 目的主机的 MAC 地址是00:05:5d:61:58:a8源主机的 MAC 地址是 00:05:5d:a1:b8:40上层协议类型 0x0806 表示 ARPARP 帧(28 字节) 0000: 00 01 —— 硬件类型 0x0001 表示以太网0010: 08 00 06 04 00 02 00 05 5d a1 b8 40 c0 a8 00 02 —— 协议类型 0x0800 表示 IP 协议硬件地址(MAC地址)长度为 6协议地址(IP地址)长度为 4op 为 0x0002 表示应答源主机 MAC 地址为 00:05:5d:a1:b8:40源主机 IP 地址为 c0 a8 00 02(192.168.0.2)0020: 00 05 5d 61 58 a8 c0 a8 00 37 —— 目的主机MAC地址为 00:05:5d:61:58:a8目的主机 IP 地址为c0 a8 00 37(192.168.0.55) 填充位(18 字节) 0020: 00 77 31 d2 50 100030: fd 78 41 d3 00 00 00 00 00 00 00 00
其他的数据包格式也以差不多的方式进行发送和应答。 注意通过 IP 地址可以确定同一网段中唯一的一台主机主机使用端口号来区分不同的应用程序。 socket 编程
传统的进程间通信借助内核提供的IPC机制进行但是只能限于本机通信若要跨机通信就必须使用网络通信(本质上借助内核-内核提供了 socket 伪文件的机制实现通信——实际上是使用文件描述符)这就需要用到内核提供给用户的 socket API 函数库。
网络字节序
在进行网络通信时一定要注意数据的字节序问题如果不使用同一的字节序发送/接收的数据可能是错误的。网络字节序分为两种
大端字节序低地址存放高位数据高地址存放低位数据小段字节序低地址存放低位数据高地址存放高位数据 如何确定本机上是大端还是小段代码测试实例如下
#include stdio.hunion {short sval;char cval[sizeof(short)];
} u1;union {int ival;char cval[sizeof(int)];
} u2;int main() {u1.sval 0x0102;// cval[0] 中存放的是 0x01 则是大端字节序否则则是小端字节序printf(u1.sval %#x, u1.cval[0] %#x, u1.cval[1] %#x\n, u1.sval, u1.cval[0], u1.cval[1]);u2.ival 0x01020304;printf(u2.ival %#x, u2.cval[0] %#x, u2.cval[1] %#x, u2.cval[2] %#x, u2.cval[3] %#x\n, u2.ival, u2.cval[0], u2.cval[1], u2.cval[2], u2.cval[3]);return 0;
}在网络传输中使用的是大端字节序如果机器用的是小端法则需要进行大小端的转换。一般使用以下 4 个函数
#include arpa/inet.h// 将主机字节序转换为网络字节序
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);// 将网络字节序转换为主机字节序
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);其中 h 表示主机 hostn 表示网络 networks 表示 shortl 表示 long。
socket 编程的 API 函数
socket 函数
socket 函数用来创建一个网络套接字
#include sys/types.h
#include sys/socket.h/*** param:* domain: 协议版本有 AF_INET 表示 IPV4AF_INET6 表示 IPV6 等* type: 协议类型主要有 SOCK_STREAM 和 SOCK_DGRAM分别表示 TCP 和 UDP* protocol: 一般填 0, 表示使用对应类型的默认协议* return: 成功返回大于 0 的文件描述符失败返回 -1*/
int socket(int domain, int type, int protocol);当调用 socket 函数以后返回一个文件描述符内核会提供与该文件描述符相对应的读和写缓冲区同时还有两个队列分别是请求连接队列和已连接队列。
bind 函数
bind 函数将 socket 创建的文件描述符与 IPport 绑定
#include sys/types.h
#include sys/socket.h/*** param:* sockfd: 调用 socket 函数返回的文件描述符* addr: 本地服务器的 IP 地址和 PORT* addrlen: addr 变量的占用的内存大小* return: 成功返回 0失败返回 -1*/
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);sockaddr 是一个保存 IP 和 PORT 的结构体其原型如下
struct sockaddr {sa_family_t sa_family;char sa_data[14];
}由于 sockaddr 在保存地址数据的时候比较繁琐有了 sockaddr_in 结构其结构原型如下
struct sockaddr_in {sa_family_t sin_family; /* address family: AF_INET */in_port_t sin_port; /* port in network byte order */struct in_addr sin_addr; /* internet address */
};/* Internet address. */
struct in_addr {uint32_t s_addr; /* address in network byte order */
};在实际的使用中使用更多的是 sockaddr_in 结构类型传参时进行类型转换即可这两个结构体的大小是一样的。 listen 函数
listen 函数将套接字由主动改为被动
#include sys/types.h
#include sys/socket.h/*** param:* sockfd: 调用 socket 函数返回的文件描述符* backlog: 同时请求连接的最大来凝结个数(进入连接队列)* return: 成功返回 0失败返回 -1*/
int listen(int sockfd, int backlog);accept 函数
accept 函数从连接队列中获取一个连接如何连接队列中没有连接则会阻塞等待
#include sys/types.h
#include sys/socket.h/*** param:* sockfd: 调用 socket 函数返回的文件描述符* addr: 传出参数保存客户端的地址信息* addrlen: 传入传出参数addr 变量所占内存空间大小* return: 成功返回获取连接客户端的文件描述符失败返回 -1*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);内核会负责将请求队列中的连接拿到已连接队列中。
connect 函数
connect 函数是主动向指定的 IP 和 PORT 地址发送连接请求。
#include sys/types.h
#include sys/socket.h/*** param:* sockfd: 调用 socket 函数返回的文件描述符* addr: 服务端的地址信息* addrlen: addr 变量所占内存空间大小* return: 成功返回 0失败返回 -1*/
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);在发送请求连接前需要确定服务器的地址信息其中包括以点分十进制表示的 IP 地址将其转换成网络字节序的 IP 地址方式使用以下的函数
#include arpa/inet.h/*** description: 将 IPV4 或 IPV6 的地址从点分十进制的 IP 转换为网络字节序* param:* af: AF_INET 或 AF_INET6* src: 字符串形式的点分十进制的 IP 地址* dst: 存放转换后的变量的地址* return: 成功返回指向 dst 的指针失败返回 NULL*/
int inet_pton(int af, const char *src, void *dst);/*** description: 将 IPV4 或 IPV6 的地址从网络字节序转换为点分十进制的 IP* param:* af: AF_INET 或 AF_INET6* src: 网络的整形的 IP 地址* dst: 转换后的 IP 地址一般为字符串数组* size: dst 的长度* return: 成功返回指向 dst 的指针失败返回 NULL*/
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);服务器端和客户端的开发流程
使用 socket 的 API 函数编写服务端和客户端程序的步骤图示 服务器端的实现
#include stdio.h
#include stdlib.h
#include string.h
#include unistd.h
#include sys/socket.h
#include sys/types.h
#include arpa/inet.h#define BUFFERSIZE 1024int main(int argc, char *argv[]) {if (2 ! argc) {fprintf(stderr, Usage: %s port\n, argv[0]);exit(EXIT_FAILURE);}// 创建套接字int sfd socket(AF_INET, SOCK_STREAM, 0);if (-1 sfd) {perror(socket() error);exit(EXIT_FAILURE);}// 绑定 IP 和 PORTstruct sockaddr_in serv_addr;serv_addr.sin_family AF_INET;serv_addr.sin_addr.s_addr htonl(INADDR_ANY);serv_addr.sin_port htons(atoi(argv[1]));if (-1 bind(sfd, (struct sockaddr *)serv_addr, sizeof(serv_addr))) {perror(bind() error);close(sfd);exit(EXIT_FAILURE);}// 将套接字由主动态变为被动态if (-1 listen(sfd, 2)) {close(sfd);perror(listen() error);exit(EXIT_FAILURE);}// 从连接获得一个连接没有连接则阻塞等待连接队列取完则退出struct sockaddr_in clnt_addr;socklen_t addr_len sizeof(clnt_addr);int cfd accept(sfd, (struct sockaddr *)clnt_addr, addr_len);if (-1 cfd) {close(sfd);perror(accept() error);exit(EXIT_FAILURE);}int rlen;char message[BUFFERSIZE] {0};// 开始读取和发送数据while (1) {memset(message, 0, BUFFERSIZE);rlen read(cfd, message, BUFFERSIZE);if (0 rlen) {printf(client %d is disconnected\n, cfd);break;} else if (0 rlen) {perror(read() error);break;}printf(READ: %s, message);write(cfd, message, rlen);}close(cfd);close(sfd);return 0;
}客户端的实现
#include stdio.h
#include stdlib.h
#include string.h
#include unistd.h
#include sys/socket.h
#include sys/types.h
#include arpa/inet.h#define BUFFERSIZE 1024int main(int argc, char *argv[]) {if (3 ! argc) {fprintf(stderr, Usage: %s ip port\n, argv[0]);exit(EXIT_FAILURE);}// 创建套接字int cfd socket(AF_INET, SOCK_STREAM, 0);if (-1 cfd) {perror(socket() error);exit(EXIT_FAILURE);}// 向服务器端发送连接请求struct sockaddr_in clnt_addr;clnt_addr.sin_family AF_INET;inet_pton(AF_INET, argv[1], clnt_addr.sin_addr.s_addr);clnt_addr.sin_port htons(atoi(argv[2]));if (-1 connect(cfd, (struct sockaddr *)clnt_addr, sizeof(clnt_addr))) {close(cfd);perror(connect() error);exit(EXIT_FAILURE);}char message[BUFFERSIZE] {0};while (1) {memset(message, 0, BUFFERSIZE);printf(Please input message(q/Q to quit): );fgets(message, BUFFERSIZE-1, stdin);if (!strcmp(message, Q\n) || !strcmp(message, q\n))break;int wlen write(cfd, message, sizeof(message));printf(WRITE: %s, message);int rlen read(cfd, message, BUFFERSIZE);if (rlen 0) {perror(read() error);break;}printf(READ: %s, message);}close(cfd);return 0;
}