admin 管理员组

文章数量: 887021

Linux-网络编程-学习笔记(20):网络基础与编程实践

一、网络基础

1. 网络通信概述

网络是用物理链路将各个孤立的工作站或主机相连在一起,组成数据链路,从而达到资源共享和通信的目的。通信是人与人之间通过某种媒体进行的信息交流与传递。网络通信是通过网络将各个孤立的设备进行连接,通过信息交换实现人与人,人与计算机,计算机与计算机之间的通信。

站在进程的层面来说,网络之间的通信其实就是位于网络中不同主机上面的2个进程之间的通信
以2台设备的QQ之间通信为例:

从图中可以看出,网络通信是分层的:
(1)硬件部分:网卡(负责通过网线或者WIFI的方式与外部网络进行通信)
(2)操作系统底层:网卡驱动(负责驱动网卡以及打包数据的操作)
(3)操作系统API:socket接口(应用软件去操作网卡驱动的API)
(4)应用层:低级(直接基于socket接口编程)
(5)应用层:高级(基于网络通信应用框架库)
(6)应用层:更高级(http、网络控件等)

2. 网络通信基础知识

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)。
(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位二进制,而在人机交互中使用点分十进制方式显示。

二进制方式0xffffffff0xC0A80166/0x6601A8C0本质
点分十进制方式255.255.255.255192.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.1 网络的分层结构
因为网络是一种非常复杂的通信方式,所以要通过分层来进行开发难度的降低。因此我们在研究网络通信时,一定要在同一个层次进行研究,不能跨层次研究,比如分析客户端和服务器的收发时,要分析API层次时,两部分都要统一在这个层次进行分析,而不能是一端分析API接口,另一端却去分析驱动了。一般情况下,我们在网络编程时最关注的是应用层,传输层只需要了解即可。

1.2 BS和CS
(1)CS架构介绍(client server,客户端服务器架构)。比如QQ,360网盘之类的(在电脑上或手机上用软件登录的)。
(2)BS架构介绍(broswer server,浏览器服务器架构)。比如在线版的QQ,网页版360网盘(用浏览器打开的)。

2. TCP协议

传输控制协议(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. 根据TCP协议搭建网络链接

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:先检测本设备是大端还是小端,然后自动转为大端模式。(在网络编程中,默认都使用大端模式

4. 实现网络通信

(1)设计网络通信主要包括2个部分,一个是客户端:负责去连接服务器。另一个是服务器:负责监听客户端的连接并配合它。
(2)客户端和服务器原则上都可以任意的发和收,但是实际上双方必须配合:client发的时候server就收,而server发的时候client就收。但是client和server之间的通信是异步的,所以需要依靠应用层协议来解决。
(3)规定连接建立后由客户端主动向服务器发出1个请求数据包,然后服务器收到数据包后回复客户端一个回应数据包,这就是一个通信回合,整个连接的通信就是由N多个回合组成的。同时双发发送的数据包格式也要有一定要求。

下面以一个例子展示网络编程:客户端向服务器注册学生的基本信息(发送一个数据包),服务器回应一个数据包表示接收完成(展示学生的基本信息)。

客户端代码:client.c

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>

#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 <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>

#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;
}

本文标签: 学习笔记 网络编程 基础 网络 Linux