马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
1. 计算机网络的配景
所谓的 “局域网” 和 “广域网” 只是一个相对的概念。
2. 认识网络协议
我们都知道,协议就是“两边”都必须遵守的约定。
那什么叫网络协议呢?
计算机生产厂商有很多,计算机操作体系也有很多,计算机网络硬件设备还是有很多。
怎样让这些差异厂商之间生产的计算机能够相互顺畅的通讯呢?就需要有人站出来,约定一个共同的标准,大家都来遵守,这就是网络协议。
无论是电脑、手机还是其他设备,只要它们遵循相同的网络协议,就能在网络上相互通讯、交换数据。
2.1 协议分层
协议本质也是软件,在筹划上为了更好的进行模块化、解耦合,也是被筹划成为层状结构的。
在实际生活中,我们可以通过电话直接和朋友交换,因此我们认为“同层协议可以直接通讯”;但本质上,同层协议之间没有直接通讯,而是各自利用下层结构的能力,完成通讯的,我们本质上是在对电话发言。
通过上图我们发现,任何一层的变革,只要遵守协议,都不会影响别的层,所以分层可以实现解耦合,让软件维护的成本更低。
很多书中都讲了7层不好,其实在网络角度, OSI 定的协议7层模型其实非常完善,但是在实际操作的过程中,会话层、表示层是不大概接入到操作体系中的所以在工程实践中,终极落地的是 5 层协议。
TCP/IP 是一组协议的代名词,它还包括许多协议,组成了 TCP/IP 协议族。
TCP/IP 通讯协议采用了 5 层的层级结构, 每一层都呼叫它的下一层所提供的网络来完成本身的需求。
- 物理层:负责光/电信号的通报方式。好比如今以太网通用的网线(双绞 线)、 早 期以太网采用的的同轴电缆(如今重要用于有线电视)、 光纤,如今的 wifi 无线网利用 电磁波等都属于物理层的概念。 物理层的能力决定了最大传输速率、 传输间隔、 抗 干扰性等。集线器(Hub)工作在物理层。
- 数据链路层: 负责设备之间的数据帧的传送和辨认。比方网卡设备的驱动、帧同步(就是说从网线上检测到什么信号算作新帧的开始)、 辩论检测(假如检测到辩论就 主动重发)、数据差错校验等工作。有以太网、令牌环网、无线 LAN 等标准。 交换机 (Switch)工作在数据链路层.
- 网络层: 负责地址管理和路由选择。 比方在 IP 协议中, 通过IP 地址来标识一台 主机,,并通过路由表的方式规划出两台主机之间的数据传输的线路(路由)。路由器 (Router)工作在网路层.
- 传输层: 负责两台主机之间的数据传输。如传输控制协议 (TCP), 能够确保数据 可靠的从源主机发送到目标主机.
- 应用层: 负责应用步调间沟通, 如简朴电子邮件传输(SMTP)、文件传输协 议(FTP)、 网络远程访问协议(Telnet) 等.
网络编程重要就是针对应用层
物理层我们考虑的比力少, 我们只考虑软件相关的内容,因此很多时间我们直接称为TCP/IP 四层模型。
- 对于一台主机,它的操作体系内核实现了从传输层到物理层的内容
- 对于一台路由器,它实现了从网络层到物理层
- 对于一台交换机,它实现了从数据链路层到物理层
- 对于集线器,它只实现了物理层
但是并不绝对,很多交换机也实现了网络层的转发;很多路由器也实现了部分传输层的内容(好比端口转发)。
上面的内容,我们只是懂了一些基本概念,还是达不到我们的目标,下面我们再次重新明白协媾和协议分层:
那么为什么要有 TCP/IP 协议?
- 首先,即便是单机,你的计算机内部,其实都是存在协议的,好比:其他设备和内存通讯,会有内存协议。 其他设备和磁盘通讯,会有磁盘相关的协议,好比:SATA, IDE, SCSI 等。 只不外我们感知不到罢了。 而且这些协议都在本地主机各自的硬件中,通讯的成本、题目比力少。
- 其次,网络通讯最大的特点就是主机之间变远了。 任何通讯特性的变革,一定会带来新的题目,有题目就得解决题目,所以需要新的协议。
所以,协议产生的本质就是:解决因间隔变远而产生的通讯题目。
由于间隔变远产生的题目多种多样,所以要将它们分类,即协议要分层。
2.2 OS与网络的关系
我们都清楚的知道,操作体系各式各样,没有标准,但是为什么差异的操作体系间能够通讯呢?
那是因为差异的操作体系中,网络协议栈必须按照TCP/IP协议标准来实现,即差异操作体系利用的网络协议栈一样,那就可以通讯了。
所以,操作体系与网络的关系是:操作体系内要实现网络相关的功能,即网络是操作体系的一个模块。
那么到底什么是协议呢?
既然操作体系要实现网络的功能,那OS内就会存在大量的协议,OS就要管理协议,所以协议本质就是数据结构,即结构体。
题目: 主机 B 能辨认 data, 而且正确提取 a=10, b=20, c=30 吗? 回复: 答案是肯定的!
因为两边都有同样的结构体类型 struct protocol。 也就是说,用同样的代码实现协议,用同样的自界说数据类型,天然就具有”共识“, 能够辨认对方发来的数据,这不就是约定吗?
关于协议的朴素明白: 所谓协议, 就是通讯两边都认识的结构化的数据类型(结构体)
3. 网络传输基本流程
3.1 局域网通讯流程
首先,两台主机在同一个局域网,是否能够直接通讯? - - 是的,一台主机发送的信息,在同一个局域网下的全部主机,都可以收到。
只不外在局域网上的每台主机,要有唯一的标识来包管主机的唯一性: mac 地址。
mac地址长度为 48 位,即 6 个字节。一般用 16 进制数字加上冒号的情势来表示(比方:08:00:27:03:fb:19), 在网卡出厂时就确定了,不能修改,mac 地址通常是唯一的。
以太网中,任何时刻,只允许一台呆板向网络中发送数据
- 假如有多台同时发送, 会发生数据干扰, 我们称之为数据碰撞
所以,以太网就是临界资源,是互斥访问的,但是不加锁,先用,碰撞堕落了再说。
- 全部发送数据的主秘密进行碰撞检测和碰撞制止(以包管数据读写的原子性)
- 没有交换机的环境下,一个以太网就是一个碰撞域
- 局域网通讯的过程中,主机对收到的报文确认是否是发给本身的,是通过目标mac地址在数据链路层判断的,若不是,直接丢弃,上层用户感知不到。
在数据传输时,要颠末每一层;其中每层都有协议,所以当我进行数据传输流程的时间,要进行封装和解包
报头部分(对应协议层的结构体字段)我们一般叫做报头;除了报头 剩下的叫做有效载荷,故报文 = 报头 + 有效载荷。
我们在明确一下差异层的完备报文的叫法,差异的协议层对数据包有差异的称谓
- 在传输层叫做段(segment),
- 在网络层叫做数据报 (datagram),
- 在链路层叫做帧(frame).
- 应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部,称为封装。
- 首部信息中包含了一些类似于首部有多长,载荷有多长,上层协议是什么等信息。
- 数据封装成帧后发到传输介质上,到达目标主机后每层协议再剥掉相应的首部,根据首部中的 “上层协议字段” 将数据交给对应的上层协议处理。
从今天开始, 我们学习任何协议, 都要先宏观上建立这样的认识:
- 要学习的协议,是怎样做到解包的? 只有明确了解包, 封包也就能明白
- 要学习的协议,是怎样做到将本身的有效载荷, 交付给上层协议的?
3.2 跨网络通讯流程
IP 地址是在 IP 协议中,用来标识网络中差异主机的地址。
- 对于 IPv4 来说, IP 地址是一个 4 字节,32 位的整数;
- 我们通常也利用 “点分十进制” 的字符串表示 IP 地址,比方 192.168.0.1;用点分割的每一个数字表示一个字节,范围是 0 - 255;
但是,你们又没有一个疑问:上面我们所说的mac地址也能唯一标识一个主机,但二者有什么区别呢?
唐僧每颠末一个城池,都会询问国王下一个地方它去哪,它给国王说的是:我从长安来,到西天去。
国王根据唐僧的目标地,告诉唐僧间隔当前位置近来的下一个城池。
所以:
- IP地址是终极目标,用来标识整个网络中标识唯一性。
- MAC地址是近期目标,用来标识主机在局域网中的唯一性。其受IP地址影响,根据路由器完成mac地址的切换(mac地址仅在局域网中有效)
跨网段的主机的数据传输,数据从一台计算机到另一台计算机传输过程中要颠末一个或多个路由器。
为什么要去目标主机, 先要走路由器?
联合封装与解包,体现路由器解包和重新封装的特点
提炼 IP 网络的意义和网络通讯的宏观流程
所以,IP网络层存在的意义:提供网络虚拟层,让世界的全部网络都是IP网络,屏蔽最底层网络的差异。
4. Socket 编程预备
4.1 明白源IP地址和目标IP地址
IP 在网络中,是用来标识主机的唯一性的,可以通过IP传输数据。
但是这里要思考一个题目: 数据传输到主机是目标吗? 不是的,因为数据是给人用的。
- 好比: 谈天是人在谈天,下载是人在下载,欣赏网页是人在欣赏?
- 但是人是怎么看到谈天信息的呢? 怎么执行下载任务呢? 怎么欣赏网页信息呢?通过启动的 qq,迅雷,欣赏器。
- 而启动的 qq,迅雷,欣赏器都是进程。 换句话说,进程是人在体系中的代表,只要把数据给进程,人就相当于就拿到了数据。
所以: 数据传输到主机不是目标,而是手段。 到达主机内部,再交给主机内的进程,才是目标。
那么,源IP地址和目标IP地址是干什么的呢?- - 解决两头主机的唯一性。
但是体系(主机)中,同时会存在非常多的进程, 当数据到达目标主机之后,怎么转发给目标进程呢?
4.2 端口号与Socket
端口号(port)是传输层协议的内容。
- 端口号是一个2字节16 位的整数
- 端口号用来标识一个进程,告诉操作体系,当前的这个数据要交给哪一个进程来处理
- 一个端口号只能被一个进程占用
端口号范围分别
- 0 - 1023:知名端口号, HTTP、FTP、SSH 等这些广为利用的应用层协议,他们的端口号都是固定的。
- 1024 - 65535:操作体系动态分配的端口号,客户端步调的端口号就是由操作体系从这个范围分配的。
我们之前在学习体系编程的时间,学习了 pid 表示唯逐一个进程;此处我们的端口号也是唯一表示一个进程,那么这两者之间是怎样的关系?
- 进程 PID 属于体系概念,技术上也具有唯一性,确实可以用来标识唯一的一个进程, 但是这样做, 会让体系进程管理和网络强耦合,实际筹划的时间, 并没有选择这样做。
- 另外,一个进程可以绑定多个端口号, 但是一个端口号不能被多个进程绑定
传输层协议(TCP 和 UDP)的数据段中有两个端口号,分别叫做源端口号和目标端口号,就是在描述 "数据是谁发的,要发给谁";
那么, IP 地址 + 端口号就能够标识网络上的某一台主机的某一个进程,所以网络通讯的本质是:进程间通讯,网络就是它们看到的同一份资源,我们把IP 地址 + 端口号叫做Socket(套接字)
4.3传输层的典范代表
假如我们了解了体系,也了解了网络协议栈,我们就会清楚,传输层是属于内核的,那么我们要通过网络协议栈进行通讯,肯定调用的是传输层提供的体系调用,来进行的网络通讯。
认识 TCP 协议,此处我们先对 TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识
认识 UDP 协议,此处我们也是对 UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识;
4.4 网络字节序
我们知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,大端与小端呆板通讯,要怎么办呢?
所以,必须在网络中将大小端规定好!
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址(低权值位),后发出的数据是高地址(高权值位)。
- TCP/IP协议规定:网络数据流应采用大端字节序,即低地址处是高权值位。
- 不管这台主机是大端机还是小端机,都会按照这个 TCP/IP 规定的网络字节序来发送/接收数据;
- 假如当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可
因此,网络规定:全部发送到网络上的数据,都必须是大端的
为使网络步调具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
这些函数名很好记,h表示host,n表示network,l 表示32位长整数(常用来转ip),s表示16位短整数(常用来转port)。
- 比方 htonl表示将 32 位的长整数从主机字节序转换为网络字节序,比方将 IP 地 址转换后准备发送。
- 假如主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 假如主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
5. socket 编程接口
5.1 先容
- // 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
- int socket(int domain, int type, int protocol);
- // 绑定端口号 (TCP/UDP, 服务器)
- int bind(int socket, const struct sockaddr *address, socklen_t address_len);
- // 开始监听 socket (TCP, 服务器)
- int listen(int socket, int backlog);
- // 接收请求 (TCP, 服务器)
- int accept(int socket, struct sockaddr* address, socklen_t* address_len);
- // 建立连接 (TCP, 客户端)
- int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
复制代码 在上述接口中,很多都有一个 sockaddr 的结构体,这是因为套接字的种类有很多:网络socket、本地socket(域间socket)、原始socket。
OS为了提供一个同一的体系调用接口,筹划了sockaddr 的结构体
用户具体利用什么类型的socket不管,只要你传进来的指针是sockaddr类型的;OS内部为了区分是网络还是本地,在这三个结构中,大家的前两个字节用来标识地址的类型。 (这在C++中不就是继承与多态嘛)
5.2 案例演示
下面利用一个简朴的回显服务器和客户端代码,演示一些UDP中常用的接口
服务器端:
该体系调用的返回值是一个文件描述符,所以创建socket本质是创建了一个文件。
- // 1. 创建UDP网络socket
- int _socket = ::socket(AF_INET, SOCK_DGRAM, 0);
复制代码 在绑定socket之前,应初始化一下socket;但在上面我们说了,socket的种类很多,我们这里利用网络socket为例,所以应该先创建一个sockaddr_in类型的socket。
在初始化sockaddr_in之前,需要留意port与IP的网络字节序题目
- struct sockaddr_in inetSocket;
- bzero(&inetSocket,sizeof(inetSocket)); //sockaddr_in全部清零,也可用memset
- inetSocket.sin_family = AF_INET;
- inetSocket.sin_port = ::htons(_port); //端口-->网络字节序
- inetSocket.sin_addr.s_addr = ::inet_addr(_ip.c_str()); //string -->4字节-->网络字节序
复制代码 绑定
- // 3. 设置套接字进入内核
- int n = ::bind(_socket,(sockaddr*)&inetSocket,sizeof(inetSocket));
复制代码
- struct sockaddr_in fromUser;
- socklen_t len = sizeof(fromUser);
- char buffer[1024];
- int n = ::recvfrom(_socket,&buffer,sizeof(buffer),0,(sockaddr*)(&fromUser),&len);
复制代码
- ::sendto(_socket,echoString.c_str(),echoString.size(),0,(sockaddr*)(&fromUser),len);
复制代码 Server完备代码:
- //Common.hpp
- #pragma once
- #include <stdlib.h> //exit
- #define Die(code) \
- do \
- { \
- exit(code); \
- } while (0)
-
- #define CONVERSE(src) (struct sockaddr*)(src)
- enum exitCode
- {
- SOCKET_ERR = 1,
- BIND_ERR
- };
复制代码- #pragma once
- #include <memory>
- #include <string>
- #include <cstring>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #include "Log.hpp"
- #include "Common.hpp"
- const static int g_socket = -1;
- const static int g_port = 8080;
- const static std::string g_default_ip = "127.0.0.1";
- using namespace MyLogModule;
- class UdpServer
- {
- public:
- UdpServer(uint16_t port = g_port, std::string ip = g_default_ip)
- : _socket(g_socket)
- , _port(port)
- , _ip(ip)
- , _isRunning(false)
- {
- }
- ~UdpServer()
- {
- }
- void Init()
- {
- // 1. 创建网络socket
- _socket = ::socket(AF_INET, SOCK_DGRAM, 0);
- if (_socket == -1)
- {
- LOG(FATAL) << "socket create fail";
- Die(SOCKET_ERR);
- }
- LOG(NORMAL) << "file fd:" << _socket;
- // 2. 向套接字中填充网络信息
- struct sockaddr_in inetSocket;
- bzero(&inetSocket, sizeof(inetSocket)); // sockaddr_in全部清零,也可用memset
- inetSocket.sin_family = AF_INET;
- inetSocket.sin_port = ::htons(_port); // 端口-->网络字节序
- inetSocket.sin_addr.s_addr = ::inet_addr(_ip.c_str()); // string -->4字节-->网络字节序
- // 3. 设置套接字进入内核
- int n = ::bind(_socket, CONVERSE(&inetSocket), sizeof(inetSocket));
- if (n == -1)
- {
- LOG(FATAL) << "bind fail";
- Die(BIND_ERR);
- }
- LOG(NORMAL) << "bind success";
- }
- void Start()
- {
- if (!_isRunning)
- {
- _isRunning = true;
- while (true)
- {
- struct sockaddr_in fromUser;
- socklen_t len = sizeof(fromUser);
- char buffer[1024];
- int n = ::recvfrom(_socket, &buffer, sizeof(buffer), 0, CONVERSE(&fromUser), &len);
- if (n > 0)
- {
- buffer[n] = 0;
- // 接收成功,显示消息内容,谁发来的
- uint16_t clientPort = ::ntohs(fromUser.sin_port); // 网络字节序-->端口
- std::string clientIp = ::inet_ntoa(fromUser.sin_addr); // 网络字节序-->4字节-->字符串
- std::string message = std::to_string(clientPort) + ":" + clientIp + "#:";
- message += buffer;
- LOG(DEBUG) << message;
- std::string echoString = "echo say#";
- echoString += buffer;
- // 将消息在回显给发送方
- ::sendto(_socket, echoString.c_str(), echoString.size(), 0, CONVERSE(&fromUser), len);
- }
- }
- }
- _isRunning = false;
- }
- private:
- int _socket;
- int16_t _port; // 端口
- std::string _ip; // ip地址
- bool _isRunning;
- };
复制代码 客户端
- #ifndef __UpdClient__hpp__
- #define __UpdClient__hpp__
- #include <sys/socket.h>
- #include <sys/types.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #include "Log.hpp"
- #include <memory>
- #include <string>
- using namespace MyLogModule;
- class UdpClient
- {
- public:
- UdpClient(std::string ip, std::string port)
- {
- _port = stoi(port.c_str());
- _ip = ip;
- }
- ~UdpClient()
- {
- }
- void Start()
- {
- _socket = ::socket(AF_INET, SOCK_DGRAM, 0);
- if (_socket == -1)
- {
- LOG(FATAL) << "socket create fail";
- Die(SOCKET_ERR);
- }
- // 转换为网络字节序,并设置
- struct sockaddr_in server;
- server.sin_family = AF_INET;
- server.sin_addr.s_addr = ::inet_addr(_ip.c_str());
- server.sin_port = ::htons(_port);
- while(true)
- {
- std::string message;
- std::cout << "Please enter# ";
- std::getline(std::cin,message);
-
- //客户端也要有自己的ip和port,但不需要绑定,因为OS会自动bind
- //服务端要显示的bind,是因为服务器的端口号,必须稳定!必须是众所周知且不能改变轻易改变的!
- ::sendto(_socket,message.c_str(),message.size(),0,CONVERSE(&server),sizeof(server));
- char buffer[1024];
- //虽然上面已经有了sockaddr_in server,但是可能不只有一个服务器哦
- struct sockaddr_in tmp;
- socklen_t len = sizeof(tmp);
- int n = ::recvfrom(_socket,buffer,sizeof(buffer)-1,0,CONVERSE(&tmp),&len);
- if(n > 0)
- {
- buffer[n] = 0;
- //打印服务器回写的
- std::cout << buffer << std::endl;
- }
- }
- }
- private:
- int _socket;
- uint16_t _port;
- std::string _ip;
- };
- #endif
复制代码 clientMain.cc
- #include "Common.hpp"
- #include "UdpClient.hpp"
- int main(int argc,char* argv[])
- {
- if(argc != 3)
- {
- std::cerr << "usage fail " << "Usage:" << argv[0] << "serverIp serverPort" << std::endl;
- Die(USE_ERR);
- }
- //使用ip与port构建UdpClient对象的指针
- std::shared_ptr<UdpClient> c_ptr = std::make_shared<UdpClient>(argv[1],argv[2]);
- c_ptr->Start();
- return 0;
- }
复制代码 至此,两主机就可完成网络通讯了。
检察网络服务是否启动的下令:netstat
- u:udp
- a: all
- p:pid/program
- n:num(能显示成数字的则显示)
但是当前的步调尚有一个题目,那就是假如服务器上有多张网卡,那就有多个ip地址,但是server默认的ip是127.0.0.1,这样就会导致server无法收到别的ip所搜到的信息。
因此,需要将server的ip设置为INMADDR_ANY

将server中涉及ip的位置都修改就可以了

但是上面的代码尚有点不优雅,特别是涉及ip地址与端口号那边,下面我们修改一下,直接将ip与port封装成一个对象。
- #pragma once
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #include <string>
- #include "Common.hpp"
- class InetAddr
- {
- private:
- void portNet2Host()
- {
- _port = ::ntohs(_net_addr.sin_port);
- }
- void ipNet2Host()
- {
- // 此种方式inbuffer是局部的,不会造成问题
- char inbuff[64];
- _ip = ::inet_ntop(AF_INET, &_net_addr.sin_addr, inbuff, sizeof(inbuff));
- }
- public:
- InetAddr()
- {}
- InetAddr(const struct sockaddr_in &addr)
- : _net_addr(addr)
- {
- portNet2Host();
- ipNet2Host();
- }
- // 供服务端创建自己的
- InetAddr(uint16_t port)
- : _port(port), _ip("")
- {
- _net_addr.sin_family = AF_INET;
- _net_addr.sin_port = ::htons(_port);
- _net_addr.sin_addr.s_addr = INADDR_ANY;
- }
- struct sockaddr *Getaddr() { return CONVERSE(&_net_addr); }
- socklen_t Getlen() { return sizeof(_net_addr); }
- uint16_t getPort() { return _port; }
- std::string getIP() { return _ip; }
-
- struct sockaddr_in getAddr() { return _net_addr; }
-
- ~InetAddr()
- {}
- private:
- struct sockaddr_in _net_addr;
- uint16_t _port;
- std::string _ip;
- };
复制代码
结果:

TCP接口先容:
- // 绑定端口号 (TCP/UDP, 服务器)
- int bind(int socket, const struct sockaddr *address, socklen_t address_len);
- // 开始监听 socket (TCP, 服务器)
- int listen(int socket, int backlog);
- // 接收请求 (TCP, 服务器)
- int accept(int socket, struct sockaddr* address, socklen_t* address_len);
- // 建立连接 (TCP, 客户端)
- int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
复制代码 listen:函数的第二个参数,表示最多允许有 backlog 个客户端处于毗连等候状态,假如接收到更多的毗连哀求就忽略,这里设置不会太大。
accept:
与UDP差异的是:tcp没有recvfrom与sendto方法,它直接利用read 和write;
connect:
- 客户端需要调用 connect()毗连服务器;
- connect 和 bind 的参数情势划一,区别在于 bind 的参数是本身的地址, 而connect 的参数是对方的地址。
|