网络是用物理链路将各个孤立的工作站或主机相连在一起,组成数据链路,从而达到资源共享和通信的目的。通信是人与人之间通过某种媒体进行的信息交流与传递。网络通信是通过网络将各个孤立的设备进行连接,通过信息交换实现人与人,人与计算机,计算机与计算机之间的通信。
站在进程的层面来说,网络之间的通信其实就是位于网络中不同主机上面的2个进程之间的通信。
以2台设备的QQ之间通信为例:
从图中可以看出,网络通信是分层的:
(1)硬件部分:网卡(负责通过网线或者WIFI的方式与外部网络进行通信)
(2)操作系统底层:网卡驱动(负责驱动网卡以及打包数据的操作)
(3)操作系统API:socket接口(应用软件去操作网卡驱动的API)
(4)应用层:低级(直接基于socket接口编程)
(5)应用层:高级(基于网络通信应用框架库)
(6)应用层:更高级(http、网络控件等)
2.1 OSI7层网络模型
2.2 网卡
(1)网卡是计算机用来上网的必备硬件设备,CPU靠网卡来连接外部网络。
(2)网卡是一个串转并的设备。网卡芯片和CPU之间的通信是并行通信,网络通信是一种串行、全双工、差分的通信方式。
(3)网卡用来进行数据帧的封包和拆包。网络的发送数据的时候不单纯是发送数据,而是将一定大小的数据进行打包(64kb或其他大小)发送,包包含三部分,包头、数据和包尾(其中包头包含了IP信息等)。
(4)网卡能够实现网络数据缓存和速率适配(两个电脑的传输速率不相同)。
2.3 集线器
(1)信号中继放大,相当于中继器。
(2)组成局域网络,用广播方式工作。(多台设备插到一个集线器上)
(3)注意集线器是不能用来连接外网的。
2.4 交换机
(1)包含集线器功能,但更高级。
(2)交换机中有地址表,数据包查表后直达目的通信口而不是广播。
(3)找不到目的口时广播并学习。
2.5 路由器
(1)路由器是局域网和外部网络通信的出入口。(跨局域网进行通信需要经过网关才能出去,路由器就相当于一个网关)
(2)路由器将整个internet划分成一个个的局域网,却又互相联通。(两个路由器连接下的电脑构成了两个局域网,通过路由器实现局域网间接连接)
(3)路由器对内管理子网(局域网),可以在路由器中设置子网的网段,设置有线端口的IP地址,设置dhcp功能等,因此局域网的IP地址是路由器决定的。
(4)路由器对外实现联网,联网方式取决于外部网络(如ADSL拨号上网、宽带帐号、局域网等)。这时候路由器又相当于是更高层级网络的其中一个节点而已。
路由器的WAN是对外的口,LAN是对内的口
(5)路由器相当于有2个网卡,一个对内做网关、一个对外做节点。
(6)路由器的主要功能是为经过路由器的每个数据包寻找一条最佳路径(路由)并转发出去。其实就是局域网内电脑要发到外网的数据包,和外网回复给局域网内电脑的数据包。(路由器的好坏决定路径选的好坏,即收发速度)
(7)路由器技术是网络中最重要技术,决定了网络的稳定性和速度。
2.6 DNS(Domain Name Service 域名服务)
(1)网络世界的门牌号:IP地址(例如百度的IP地址为61.135.169.125)。IP地址的缺点:难记、不直观。
(2)IP地址的替代品:域名(例如www.baidu.com)。
(3)DNS服务器就是专门提供域名和IP地址之间的转换的服务的,因此域名要购买的.
(4)我们访问一个网站的流程是:先使用IP地址(譬如谷歌的DNS服务器IP地址为8.8.8.8)访问DNS服务器(DNS服务器不能是域名,只能是直接的IP地址),查询我们要访问的域名的IP地址,然后再使用该IP地址访问我们真正要访问的网站。这个过程被浏览器封装屏蔽,其中使用的就是DNS协议。
(5)浏览器需要DNS服务,而QQ这样的客户端却不需要(因为QQ软件编程时已经知道了腾讯的服务器的IP地址,因此可以直接IP方式访问服务器)
2.7 DHCP(dynamic host configuration protocl,动态主机配置协议)
(1)每台计算机都需要一个IP地址,且局域网内各电脑IP地址不能重复,否则会地址冲突(同一个局域网内,两台设备的IP地址重复,比如都设为了192.168.1.1)。
(2)计算机的IP地址可以静态设定(自己制定IP地址,但是管理起来很麻烦),也可以动态分配(由管理员路由器给自动分配)
(3)动态分配是局域网内的DHCP服务器来协调的,很多设备都能提供DHCP功能,譬如路由器。
(4)动态分配的优势:方便接入和断开、有限的IP地址得到充分利用。
2.8 NAT(network address translation,网络地址转换协议)
(1)IP地址分为公网IP(internet范围内唯一的IP地址)和私网IP(内网IP),局域网内的电脑使用的都是私网IP(常用的就是192.168.1.xx)
(2)网络通信的数据包中包含有目的地址的IP地址。
这里以获取百度某张图片为例:首先子网中的某台设备的IP地址为192.168.1.1,它所连接的路由器的IP地址为172.1.1.1,那么设备想要从百度获取一张图片,一定是要向百度网站发送一个请求命令,这个命令就是一个数据,那么在发送前网卡会将数据进行打包(包头、数据、包尾),其中包头保存了公网IP(路由器)和私网IP(设备)和目的地IP(百度服务器),然后通过路由器规划路径后发送到百度的服务器上,服务器进行数据的解析,得知是要获取某张图片,于是将图片数据进行打包(仍然是3部分),其中包头中的目的地址和本地地址取反(发送地变为接收地,接收地变为发送地),将图片发送回网卡,路由器进行判断,知道该图片应该发送给192.168.1.1对应的设备,最终该设备收到了图片。
(3)当局域网中的主机要发送数据包给外网时,路由器要负责将数据包头中的局域网主机的内网IP替换为当前局域网的对外外网IP。这个过程就叫NAT。
(4)NAT的作用是缓解IPv4的IP地址不够用问题,但只是类似于打补丁的形式,最终的解决方案还是要靠IPv6。
(6)NAT穿透:P2P下载方式叫做一种穿透,服务器作为中介,让两台内网相连的技术叫做穿透。
这里以迅雷下载为例:假设我要下载一张图片(局域网A的设备1),如果从百度上下载这张图片时,需要前一个例子那样,走很长一段距离才能到达百度服务器,这无疑会浪费很多时间。但是P2P下载方式为我们提供了一种点对点的方式下载,也就是如果这张图片在另外一台设备上有(局域网B的设备1),同时我的设备和那台设备都连接了训练服务器,那么迅雷服务器会为我们两个局域网之间构成了一条通路(本来两个局域网互相不知道IP
是无法进行连接的,但是如果2台设备都连接到了迅雷服务器,那么迅雷服务器会自动安排一种连接通道),这样我就可以直接到那台设备上下载图片。通过缩短了距离来提高了下载速度。如果同时有100台设备上都有这张图片,那么我的设备就与这100台设备都构成了通路,从而实现了一种并行下载,极大程度地提高了下载速度。这就好像本来两个局域网之间有一堵墙隔着,P2P的下载方式穿透了这堵墙,所以称为NAT穿透。
2.9 IP地址分类(IPv4)
(1)IP地址实际是一个32位二进制构成,在网络通信数据包中就是32位二进制,而在人机交互中使用点分十进制方式显示。
二进制方式 | 0xffffffff | 0xC0A80166/0x6601A8C0 | 本质 |
---|---|---|---|
点分十进制方式 | 255.255.255.255 | 192.168.1.102 | 方便人看的 |
(2)IP地址中32位实际包含2部分,分别为:网络地址和主机地址。子网掩码用来说明网络地址和主机地址各自占多少位。
可以8位表示网络,24位表示主机;也可以16位表示网络,16位表示主机;14为表示网络,18位表示主机。下面表格为用子网掩码表示的2种:
子网掩码:255.255.255.0 | 前24位为网络地址,后8位为主机地址 |
---|---|
子网掩码:255.255.0.0 | 前16位为网络地址,后16位为主机地址 |
网络地址决定了这种网络中一定可以有多少个网络,主机地址决定了该子网下最多能有多少主机。譬如子网掩码为255.255.255.0时表示我们这一种网络一共最多可以有224个,每个这种网络中可以有28个主机。
如果子网掩码为255.255.0.0时,表示我们这种网络可以有216个网络,每个这种网络中最多可以有216个主机。
网络地址用来表示子网,主机地址是用来表示子网中的具体某一台主机的。
(3)由网络地址和主机地址分别占多少位的不同,将IP地址分为5类,最常用的有3类(A类、B类和C类)
(4)127.0.0.0用来做回环测试loopback
(5)判断2个IP地址是否在同一子网内的方法是:查看2个IP地址的网络标识一样,那么就处于同一网络。
网络标识 = IP地址 & 子网掩码
例如:192.168.1.4和192.168.12.5,如果子网掩码是255.255.255.0那么不在同一网段,如果子网掩码是255.255.0.0那么就在同一个网段。
1.1 网络的分层结构
因为网络是一种非常复杂的通信方式,所以要通过分层来进行开发难度的降低。因此我们在研究网络通信时,一定要在同一个层次进行研究,不能跨层次研究,比如分析客户端和服务器的收发时,要分析API层次时,两部分都要统一在这个层次进行分析,而不能是一端分析API接口,另一端却去分析驱动了。一般情况下,我们在网络编程时最关注的是应用层,传输层只需要了解即可。
1.2 BS和CS
(1)CS架构介绍(client server,客户端服务器架构)。比如QQ,360网盘之类的(在电脑上或手机上用软件登录的)。
(2)BS架构介绍(broswer server,浏览器服务器架构)。比如在线版的QQ,网页版360网盘(用浏览器打开的)。
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
2.1 TCP协议的作用
(1)TCP协议工作在传输层,对上服务socket接口(API),对下调用IP层。(我们只需要通过调用socket接口实现数据收发,其内部安排由TCP来完成)
(2)TCP协议面向连接,通信前必须先3次握手建立连接关系后才能开始通信。(比如打电话必须你拨号,对面接听才能够进行双向的语音通信)
(3)TCP协议(像是一个快递公司)提供可靠传输,不怕丢包、乱序等。
2.2 TCP如何保证可靠传输
(1)TCP在传输有效信息前要求通信双方必须先握手,建立连接才能通信(比如通过打电话联系一个人时,通过收到了对方的回应从而确定对方收到了消息;而通过QQ只是发送了消息过去,对方是否看到这里不清楚)
(2)TCP的接收方收到数据包后会ack给发送方,若发送方未收到ack会丢包重传(每一次发送都要有回应,从而确保发送信息的被收到)
(3)TCP的有效数据内容会附带校验,以防止内容在传递过程中损坏(就好像快递公司给包裹加上了某种保护措施)
(4)TCP会根据网络带宽来自动调节适配速率,自动调整发送包的大小和一次发多少个包等……(滑动窗口技术)
(5)发送方会给各分割报文编号,接收方会校验编号,一旦顺序错误即会重传(传的数据顺序不能乱)
2.3 TCP的三次握手
TCP建立连接需要三次握手,这是TCP协议内部自动完成的,我们只需要调用对应的API进行收发即可。
建立连接的条件:服务器listen时客户端主动发起connect。
建立过程:SYN是一个同步信号,客户端发起完这个SYN信号 [第一次] 后就主动进入到了SYN-SENT(请求连接)状态,服务器收到信号后,就会进入到SYN-RCVD(同步收到)状态并且回复一个SYN+ACK信号 [第二次] ,客户端在SYN-SENT接收到SYN+ACK信号后会回应一个ACK信号 [第三次] ,并且将自身状态变为ESTAB-LISHED(建立服务)。服务器收到了ACK信号,也会进入到ESTAB-LISHED,从而建立连接。客户端和服务器之间可以进行双向通信。
2.4 TCP的四次挥手
TCP断开连接需要四次挥手。
断开连接的条件:服务器或者客户端都可以主动发起关闭。
断开过程:假设客户端先向其TCP发出连接释放报文段,并停止再发送数据,主动关闭TCP连接。客户端发送释放报文FIN [第一次] ,此时客户端进入FIN-WAIT-1(终止等待1)状态,等待服务器的确认。服务器收到释放报文后发出确认报文ACK [第二次] ,然后服务器就进入CLOSE-WAIT(关闭等待)状态。TCP服务器进程这时应通知高层进程,因而从客户端到服务器这个方向的连接就释放了,这时的TCP连接处于半关闭状态,即客户端已经没有数据要发送了,但服务器若发送数据,客户端仍要接收。也就是说,从服务器到客户端这个方向的连接并未关闭。这个状态可能会持续一些时间。客户端收到来自服务器的确认后,就进入FIN-WAIT-2(终止等待2)状态,等待服务器发出的连接释放报文段。若服务器已经没有要向客户端发送的数据,其应用进程就通知TCP释放连接。这时服务器发出的连接释放报文FIN,并且还附带上次已发送过的确认号ACK [第三次] 。这时服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。客户端在收到服务器的连接释放报文段后,发送确认报文ACK [第四次] 。然后进入到TIME-WAIT(时间等待)状态(请注意:现在TCP连接还没有释放掉。必须经过时间等待计时器设置的时间2MSL(MSL:最长报文段寿命)后,客户端才进入到CLOSED状态)。服务器收到ACK也就如到CLOSED状态。
3.1 基于TCP通信的服务模式
(1)首先搭建的网络连接主要分为两部分:客户端和服务器。
客户端:搭建socket接口,通过connect去向服务器发起连接。
服务器:搭建socket接口,通过bind绑定IP,然后调用listen来进入监听状态。
(2)服务器收到并同意客户端接入后会建立TCP连接,然后双方开始收发数据,收发时是双向的,而且双方均可发起,同时双方均可发起关闭连接。
(3)常见的使用了TCP协议的网络应用:http(相当于一个应用程序,用来传输文本信息)、ftp、QQ服务器和mail服务器。这些需要很高可靠的应用,底层都是基于TCP协议的。
3.2 常用的网络编程函数
(1)socket:socket函数类似于open,用来打开一个网络连接,如果成功则返回一个网络文件描述符(int类型),之后我们操作这个网络连接都通过这个网络文件描述符。
(2)bind:用来进行绑定的函数,把本地的IP地址和socket进行绑定。功能类似于fctrl函数,是用来改变属性的函数。
(3)listen:监听一个端口,监听的是在bind时绑定的那个地址。
PS:端口号,实质就是一个数字编号,用来在我们一台主机中(主机的操作系统中)唯一的标识一个能上网的进程。端口号(精确到电脑中的某个进程)和IP地址(精确到某个电脑)一起会被打包到当前进程发出或者接收到的每一个数据包中。每一个数据包将来在网络上传递的时候,内部都包含了发送方和接收方的信息(就是IP地址和端口号),所以IP地址和端口号这两个往往是打包在一起不分家的。
(4)accept:返回值是一个fd,accept正确返回就表示我们已经和前来连接我的客户端之间建立了一个TCP连接了,以后我们就要通过这个连接来和客户端进行读写操作,读写操作就需要一个fd,这个fd就由accept来返回了。(阻塞的位置)
PS:socket返回的fd叫做监听fd,是用来监听客户端的,不能用来和任何客户端进行读写;accept返回的fd叫做连接fd,用来和连接那端的客户端程序进行读写。
(5)connect:用来连接服务器的(客户端那边用)。
(6)send/write:发送数据。(send比write就多了一个flag,只有支持一些特殊协议时会用到flag,普通情况下用0即可)
(7)recv/read:接收数据。(在网络中发送有点像是写文件,在网络中接收有点像是收文件)
(8)inet_aton:点分十进制转换为32位二进制形式。(inet_pton原理相同,只是支持IPv6)
(9)inet_ntoa:32位二进制转换为点分十进制。(inet_ntop原理相同,支持IPv6)
(10)inet_addr:先检测本设备是大端还是小端,然后自动转为大端模式。(在网络编程中,默认都使用大端模式)
(1)设计网络通信主要包括2个部分,一个是客户端:负责去连接服务器。另一个是服务器:负责监听客户端的连接并配合它。
(2)客户端和服务器原则上都可以任意的发和收,但是实际上双方必须配合:client发的时候server就收,而server发的时候client就收。但是client和server之间的通信是异步的,所以需要依靠应用层协议来解决。
(3)规定连接建立后由客户端主动向服务器发出1个请求数据包,然后服务器收到数据包后回复客户端一个回应数据包,这就是一个通信回合,整个连接的通信就是由N多个回合组成的。同时双发发送的数据包格式也要有一定要求。
下面以一个例子展示网络编程:客户端向服务器注册学生的基本信息(发送一个数据包),服务器回应一个数据包表示接收完成(展示学生的基本信息)。
客户端代码:client.c
#include
#include
#include /* See NOTES */
#include
#include
#include
#define SERADDR "192.168.1.104" // 服务器开放给我们的IP地址和端口号
#define SERPORT 9003
//发送、接收缓冲区
char sendbuf[100];
char recvbuf[100];
#define CMD_REGISTER 1001 // 注册学生信息
#define CMD_CHECK 1002 // 检验学生信息
#define CMD_GETINFO 1003 // 获取学生信息
#define STAT_OK 30 // 回复ok
#define STAT_ERR 31 // 回复出错了
typedef struct commu
{
char name[20]; // 学生姓名
int age; // 学生年龄
int cmd; // 命令码
int stat; // 状态信息,用来回复
}info;
int main(void)
{
int sockfd = -1, ret = -1;
//这个结构体是网络编程接口中用来表示一个IP地址的,
//这个IP地址是兼容IPv4和IPv6的
struct sockaddr_in seraddr = {0};
struct sockaddr_in cliaddr = {0};
// 第1步:创建socket(AF_INET:使用IPv4进行通信,SOCK_STREAM:TCP)
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("socket");
return -1;
}
printf("socketfd = %d.\n", sockfd);
// 第2步:connect链接服务器,向结构体填充服务器信息
seraddr.sin_family = AF_INET; // 设置地址族为IPv4
seraddr.sin_port = htons(SERPORT); // 设置地址的端口号信息(检测大小端并调整)
seraddr.sin_addr.s_addr = inet_addr(SERADDR); // 设置IP地址
ret = connect(sockfd, (const struct sockaddr *)&seraddr, sizeof(seraddr));
if (ret < 0)
{
perror("connect");
return -1;
}
printf("成功建立连接\n");
while (1)
{
// 回合中第1步:客户端给服务器发送信息
info st1;
printf("请输入学生姓名\n");
scanf("%s", st1.name);
printf("请输入学生年龄");
scanf("%d", &st1.age);
st1.cmd = CMD_REGISTER;
ret = send(sockfd, &st1, sizeof(info), 0);
printf("已发送%s学生的信息\n",st1.name);
// 回合中第2步:客户端接收服务器的回复
memset(&st1, 0, sizeof(st1));
ret = recv(sockfd, &st1, sizeof(st1), 0);
// 回合中第3步:客户端解析服务器的回复,再做下一步定夺
if (st1.stat == STAT_OK)
{
printf("注册学生信息成功\n");
}
else
{
printf("注册学生信息失败\n");
}
}
return 0;
}
服务器端代码:server.c
#include
#include
#include /* See NOTES */
#include
#include
#include
#define SERPORT 9003
#define SERADDR "192.168.1.104" // ifconfig看到的
#define BACKLOG 100 //貌似是最大能容纳的连接数
char recvbuf[100];
#define CMD_REGISTER 1001 // 注册学生信息
#define CMD_CHECK 1002 // 检验学生信息
#define CMD_GETINFO 1003 // 获取学生信息
#define STAT_OK 30 // 回复ok
#define STAT_ERR 31 // 回复出错了
typedef struct commu
{
char name[20]; // 学生姓名
int age; // 学生年龄
int cmd; // 命令码
int stat; // 状态信息,用来回复
}info;
int main(void)
{
int sockfd = -1, ret = -1, clifd = -1;
socklen_t len = 0;
//这里的结构为sockaddr_in结构体包含sin_port和sin_addr结构体,sin_addr结构体包含s_addr
struct sockaddr_in seraddr = {0};
struct sockaddr_in cliaddr = {0};
// 第1步:先socket打开文件描述符
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("socket");
return -1;
}
printf("socketfd = %d.\n", sockfd);
// 第2步:bind绑定sockefd和当前电脑的ip地址&端口号
seraddr.sin_family = AF_INET; // 设置地址族为IPv4
seraddr.sin_port = htons(SERPORT); // 设置地址的端口号信息
seraddr.sin_addr.s_addr = inet_addr(SERADDR); // 设置IP地址
ret = bind(sockfd, (const struct sockaddr *)&seraddr, sizeof(seraddr));
if (ret < 0)
{
perror("bind");
return -1;
}
printf("bind success.\n");
// 第3步:listen监听端口
ret = listen(sockfd, BACKLOG); // 阻塞等待客户端来连接服务器
if (ret < 0)
{
perror("listen");
return -1;
}
// 第4步:accept阻塞等待客户端接入
clifd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
printf("连接已经建立,client fd = %d.\n", clifd);
// 客户端反复给服务器发
while (1)
{
info st;
// 回合中第1步:服务器收
ret = recv(clifd, &st, sizeof(info), 0);
// 回合中第2步:服务器解析客户端数据包,然后干活,
if (st.cmd == CMD_REGISTER)
{
printf("用户要注册学生信息\n");
printf("学生姓名:%s,学生年龄:%d\n", st.name, st.age);
// 在这里服务器要进行真正的注册动作,一般是插入数据库一条信息
// 回合中第3步:回复客户端
st.stat = STAT_OK;
ret = send(clifd, &st, sizeof(info), 0);
}
}
return 0;
}