一.发展历史
为了核弹
1)建立指挥机构
2)指挥机构和核弹头之间的通信链路
为了保证通信链路在核弹破坏的情况下,也能正常运作
1.局域网/广域网
1.1借助网线
1.2借助交换机/路由器
交换机上面的网口都是一样的口,这个效果就是把插在上面的设备建成一个局域网
局域网内部的主机就可以互相进行访问
交换机就是把若干个设备给组建到一个局域网
路由器有两类端口:
WAN口:通过wan口连接到另一个局域网
LAN口:在一个局域网中,通过wan口连接到另外一个局域网
路由器则是连接了两个局域网.lan口是一个.wan口是一个
可以视为A是B的局域网中一个设备
二.常见名词
1.Ip地址
描述了网络上一个主机的地址(收货地址
本质上是一个32位的整数,由于是32位的整数,不方便记忆.就按照每个字节(8位)进行点分割
2.端口号
描述了一个主机上的某个应用程序(收件人的电话)
端口号本质上是一个2个字节(16位)的无符号整数
0-65535
3306是mysql默认端口号
服务器程序在启动的时候就需要绑定上一个端口号.以便客户端程序来访问
3.协议
进行有效的通信的前提:能够明确通信协议
本质上就是约定:发出的数据是什么样的格式,接收方按照对应的格式进行解析
网络通信的本质就是传输的是光信号和电信号
通过光信号的频率(高频率/低频) 和电信号的电平(高电平/低电平)来表示0/1
学习网络原理,就是研究各种协议
4.协议分层
网络通信的过程很复杂.
如果只是通过一个协议,来约定所有细节,那么这个协议就会非常庞大
更好的方法就是把一个大的复杂的协议,拆成多个晓得,更简单的协议,每个协议就负责自身的工作.
好处一:每层协议不需要理解其他层的协议的细节(封装)
打电话的人不需要理解打电话的工作原理,制造电话的人也不用管打电话人说什么语言
好处二:把对应承担额协议换成其他协议也不影响其它层的(更好的解耦合)
打电话的人可以使用无线电也可以用有线电.也可以用英语也可以不用英语
三.网络模型
1.OSI七层网络
这种OSI七层模型只存在教科书中
实际生活中是OSI的简化版本.TCP/IP五层/四层网络模型
2.TCP/IP五层四层网络模型
1)物理层:
网络通信中的硬件设备
网线/网卡,针对硬件设备的约定,就是物理层协议所负责的范畴,需要保证所有的主机和网络设备之间,是相互匹配的
2)数据链路层
负责相邻两个设备之间的通信->一根网线相邻的两个设备
路由器和主机一是相邻的
主机1和主机2 就不是相邻的
3)网络层
负责点到点之间的通信
网络上的任意节点,到任意节点之间的通信,(不一定是相邻,更多的是指不相邻
网络层就负责在两个节点之间,规划处一个合格的路线
实际的网络环境结构非常复杂,两个点之间的路线不止一条,就需要规划最合适的一条
4)应用层
和应用程序密切相关,传输的数据用途
5)总结
①主机对应了物理层到应用层
②路由器主要就是对应物理层到网络层
③交换机主要对应的是物理层到数据链路层
网络分层重要概念就是封装和分用
不同的协议之间是如何相互配合的
3.从具体实例看每一层做了什么
假如利用qq给一个同学发送消息
用户A在键盘上输入一个"hello" 按下发送键
3.1应用层
根据用户输入的内容.把数据构成了一个应用层的协议报文
协议:是一种约定 报文:遵守了这个约定的一组数据
QQ的代码就会根据程序员设计的应用层协议,来构造一个应用层的数据报文
后端开发,工作中的一个很重要的内容,就是根据需要来设计应用层协议
应用层协议就调用操作系统提供的API(称为 socket API).把应用层的数据交给传输层就进入了操作系统内核 了
3.2传输层(操作系统内核
根据刚刚传过来的数据,基于当前使用的传输层协议,来构造出一个传输层的协议报文
传输层最典型的协议.UDP/TCP 以TCP为例
再次打包成一个TCP的数据报 = TCP报头+数据载荷(Payload,也就是一个完整的应用层数据
可以简单的把这个构造TCP报文的过程看成一个字符串拼接-->这里拼接的是二进制数据
TCP报头中含有很多重要信息最重要的就是源端口和目的端口
也就是发件人的电话和 收件人的电话
接下来就把传输层的数据报交给网络层
3.3网络层(操作系统内核
拿到了完整的传输层数据报,就会再根据当前使用的网络层协议(例如IP)再次封装
把TCP数据报构造成IP数据报
IP数据报=IP协议报头+载荷(完整的TCP/UDP的数据报)
IP协议爆头有很多重要信息
最重要的就是源IP和目的IP
相当于发现人的地址和收件人的地址
紧接着,当前的网络层协议.就会把这个IP数据报交给数据链路层
3.4数据链路层(驱动程序
把刚才的IP数据报基础上,根据当前使用的数据链路层协议,构造一个数据链路层数据报
典型的数据链路层的协议:叫做以太网.构造成一个以太网数据帧
以太网数据帧:=帧头+ip数据报_+帧尾
枕头最重要的信息:接下来传给设备的地址是什么
紧接着.数据链路层又把这个数据交给物理层
3.5物理层(硬件设备
根据刚刚的以太网数据层也就是0 1构成的数据
把这里的01变成高低电平 通过网线传输
或者把0 1变成高频/低频的电磁波,通过光纤/无限的方式传播
到了这一步.此时数据已经离开了主机,通往了下一个设备,下一个设备可能就是路由器/交换机/其他设备
从上到下,就是一个封装的过程,数据从上层协议交给下层协议
由下层协议进行封装:构成该层协议的报文
3.6物理层(硬件设备.网卡
主机B的网卡感知到了一组高低电平,然后通过这些电平翻译成0/1的一串数据
然后这一串数据0.1就是一串完整的以太网数据帧
物理层就把这个数据交给了数据链路层
3.7数据链路层(驱动
数据链路层就负责对这个数据进行解析,去掉帧头和帧尾.取出里面的IP数据报,交给网络层协议
3.8网络层(操作系统
3.9 传输层(操作系统内核
3.10应用层(应用程序
应用层就会调用Socket API从内核中读取到了这个应用层数据报
再按照应用层协议进行解析,根据解析结果给显示到窗口中
从下到上就是分用
分用是封装的逆过程
4,路途
四.网络编程套接字
是操作系统给应用程序提供的一组API 叫做socket API
socket 原义 插座
socket可以视为应用层和传输层之间的通信桥梁
1.UDP/TCP
传输层的核心协议有两种: TCP/UDP
socket API也有对应的两组
由于TCP和UDP协议差别很大.所以API也差别大
TCP.有链接,可靠传输,面向字节流.全双工
UDP:无连接,不可靠传输,面向数据报,全双工
有连接:打电话得接通才能交互数据
无连接:发微信,不需要接通也可以发数据
可靠传输;发送放直到接收方有没有接收到数据
比如:已读功能,或者打电话
不可靠传输:传输过程,发送方不知道接收方有没有收到数据
比如微信
面向字节流:以字节为单位进行传输(就是文件操作,然后把读取到的字节包装起来打印
面向数据报:以数据报为单位进行传输:一个数据包都会明确大小
全双工:一条链路:双向通信
半双工:一条链路,单项通信
2.UDP
UDP 主要涉及到两个类
一个是DatagramSocket
Datagarm就是数据报的意思
一个datagramSocket对象,对应着操作系统中socket文件
操作系统的文件指的是硬件设备或者软件资源
socket文件就对应的是"网卡"这种硬件设备
从socket文件读数据,本质上就是控制网卡读数据
从socket文件写数据,本质上就是控制网卡在上面写数据
可以类比socket文件就是一个遥控器控制操作网卡
还有一个是DatagramSocket
代表一个UDP数据报,使用UDP传输数据的基本单位
三.UDP socket
两个核心类
DatagramSocket:Datagram就是数据报的意思
这一个DatagramSocket对象.就对应着操作系统中的一个socket文件
代表着网卡硬件设备的抽象体现
receive:接收数据
send:发送数据
close:释放资源
还有一个 DatagramPacket 表示一个UDP数据包
每次发送/接收数据,都在传输一个DatagramPacket对象
四.UDP-EchoServer 回显服务器
DatagramSocket
DatagramPacket
程序一启动就可以直接读写了
回显服务---Echo 回声.请求内容是什么,得到的回应是什么
1)构建构造函数
此处构造服务器的SOCKET对象的时候,就需要显式的绑定一个端口号
端口号是用来区分一个应用程序的.主机收到网卡上的数据的时候,这个数据该给哪个程序
构造失败的可能
1)端口号被占用,同一个主机的两个程序也不能有相同的端口号
2)每个进程打开的文件个数是有上限的
2)启动服务器方法
我们需要在这个方法完成三个任务
①读取客户端发来的请求
②根据请求计算响应
③把响应写回客户端
服务端的定义就是"被动接收请求"的这一方
主动发送请求的这一方叫客户端
就是把接收的信息放在字节数组里,但是接收信息就像食堂阿姨打饭一样要你需要一个空盘子
第一部分就是构造一个空盘.第二步把空盘子放在接收函数里接收
reserve方法是有可能阻塞的
假设这里的UDP数据报最长是1024.实际上数据可能不够1024
把接收的信息写在一个string类的数据上
第一个参数不再是一个空的字节数组,第二个参数是字节数组的长度
第三个参数就是这个数据报发给谁(需要地址+电话也就是IP+port)
在当前的场景下,哪个客户端发来的请求,就把数据返回给哪个客户端
所以根据需要找到对应的地址电话
然后就把这个回复发送回去
没有需求的时候就会阻塞
//站在服务器的角度-五元组
//1.源IP:服务器程序本机的IP
//2.源端口:服务器绑定的端口(此处需要手动指定
//3.目的IP:包含在收到的数据报中.(客户端的IP)
//4.目的端口:包含在收到的数据报中(客户端的端口)
//5.协议类型:UDP
public class UDPEchoServer {
//进行网络编程,第一步就先需要准备好socket实例.这是应用层和传输层建立联机的必要条件.
private DatagramSocket socket=null;
//需要指定端口,让客户端能找到
public UDPEchoServer(int port) throws SocketException {
socket=new DatagramSocket(port);
}
//启动服务器
public void start() throws IOException{
System.out.println("启动服务器");
//UDP不用像TCP一样建立连接,直接接收从客户端来的数据即可
while(true){
//1.读取客户端发来的请求,为了接收数据就需要准备一个空的包
DatagramPacket requestPacket =new DatagramPacket(new byte[1024],1024);
socket.receive(requestPacket);//把接收的信息写入空的包里
//把DatagramPacket解析成一个String
String request =new String(requestPacket.getData(),0, requestPacket.getLength(),"UTF-8");
//2.根据请求计算响应
String response=process(request);
//3.把响应写回客户端
DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length
,requestPacket.getSocketAddress());
socket.send(responsePacket);
System.out.printf("[%s:%d] req: %s,resp:%s\n",
requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);
}
}
//回显服务
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UDPEchoServer server=new UDPEchoServer(9090);
server.start();
}
}
五.UDPEchoClient udp回显客户端
通常写代码的时候,服务器都是手动指定的,客户端由系统自动指定(系统随机分配一个
对于服务器来说,必须要手动指定,后续客户端要根据这个端口才能来访问到服务器,
如果系统随机分配.客户端根本不知道服务器端口是什么,不能访问
对于客户端来说,如果手动指定,也可以,但是系统分配更好,因为自己指定可能会占用
1)构造方法
客户端有很多,一个服务器可以给很多客户端提供连接
服务器处理每个请求,都需要消耗一定的硬件资源(CPU,内存,磁盘,带宽)
能处理多少客户端,取决于,
1.处理一个请求,消耗多少资源
2.机器一共有多少资源能用
java中一般通过性能测试的方式去检测
idea默认一个程序只能启动一个实例,再次启动会干掉其他的实例,勾选这个选项就可以启动多个实例
//站在客户端的角度-五元组
//1.源IP 本机IP
//2.源端口:系统分配的端口
//3.目的IP :服务器的端口
//4.目的IP:服务器IP
//5.协议类型udp
public class UDPEchoClient {
private DatagramSocket socket=null;
private String serverIP;
private int serverPort;
public UDPEchoClient(String ip,int port) throws SocketException {
//此处port是服务器端口
//客户端启动的时候,不需要给socket指定端口,客户端的端口是系统自己分配的
//服务器需要找客户端端口的时候,直接调用getSocketAdress就可以
socket=new DatagramSocket();
serverIP=ip;
serverPort=port;
}
public void start() throws IOException{
Scanner scanner=new Scanner(System.in);
while(true){
//1.先从控制台读取用户输入的字符串
System.out.print("->");
String request=scanner.next();
//2.把这个用户输入的内容构造成一个UDP请求,并发song
// 构造的请求包含两部分信息
// 1)数据的内容.request字符串
// 2)数据要发给谁服务器的IP+端口
DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),
request.length(), InetAddress.getByName(serverIP),serverPort);
socket.send(requestPacket);
//3.从服务器读取响应数据,并解析
DatagramPacket responsePacket=new DatagramPacket(new byte[1024],1024);
socket.receive(responsePacket);
String response=new String(responsePacket.getData(),0,responsePacket.getLength()
,"UTF-8");
//4.把响应结果写到控制台
System.out.printf("req:%s,resp:%s\n",request,response);
}
}
public static void main(String[] args) throws IOException {
UDPEchoClient udpEchoClient=new UDPEchoClient("127.0.0.1",9090);
udpEchoClient.start();
}
}
六.带翻译的UDP服务器
客户端不变,把服务器代码进行调整
主要就是调整process方法
读取请求并解析,把响应写回客户端,
关键的逻辑,就是实现这里的
根据请求处理响应
public class UDPDictServer extends UDPEchoServer{
private HashMap<String,String> dict=new HashMap<>();
//private DatagramSocket socket=null;
public UDPDictServer(int port) throws SocketException {
super(port);
//构造几个词
dict.put("cat","猫");
}
@Override
public String process(String request ) {
return dict.getOrDefault(request,"没有");
}
public static void main(String[] args) throws IOException {
UDPDictServer server=new UDPDictServer(8090);
server.start();
}
}
七 TCPEchoServer
tcp API也有两个核心的类
ServerSocket 专门给TCP服务器用的
Socket既要给服务器用,有需要给客户端用
主要通过这样的类,来描述一个socket文件.跟UDP不同,不需要用专门一个类表示传输的包
面向字节流,以字节为单位传输的
public class TCPEchoServer {
private ServerSocket serverSocket=null;
public TCPEchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
//由于TCP是由连接的,不可以一开始就可以读数据.所以需要先建立连接(打电话
//accept就是在"接电话".接电话的前提就是有人da,如果没有客户端尝试连接accept就会阻塞
//accept返回了一个socket对象.称为clientSocket.后续跟客户端之间的沟通就是通过client
//也就是说serverSocket就接电话
Thread t=new Thread(()->{
Socket clientSocket= null;
try {
clientSocket = serverSocket.accept();
} catch (IOException e) {
e.printStackTrace();
}
processConnection(clientSocket);
});
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s : %d] 客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//接下来处理请求和响应
//TCP关于tcp socket的读写和文件读写的操作是一样的
try(InputStream inputStream=clientSocket.getInputStream()){
try (OutputStream outputStream=clientSocket.getOutputStream()){
//循环处理请求,分别返回响应
Scanner scanner=new Scanner(inputStream);
while(true){
//1.读取请求
if(!scanner.hasNext()){
System.out.printf("[%s : %d] 客户端断开连接~!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
//此处用scanner更方便,如果不用也可以用inputstream的read也可以
String request=scanner.next();
//2.根据请求,计算响应
String response =process(request);
//3.把这份下响应返回给客户端
//为了方便期间,可以用PrintWriter把OutputStream包裹一下
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(response);
//上面这一步就是为了将读文件操作
printWriter.flush();
//刷新缓冲区,不刷新额度话,客户端不能第一时间看到响应结果
System.out.printf("[%s:%d] req : %s ,resp : %s\n"
,clientSocket.getInetAddress().toString(),clientSocket.getPort()
,request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
//记得挂电话
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TCPEchoServer tcpEchoServer=new TCPEchoServer(9090);
tcpEchoServer.start();
}
}
public class TCPEchoClient {
//用普通的socket即可,不需要用serverSocket
private Socket socket=null;
public TCPEchoClient(String serverIP,int serverPort) throws IOException {
//调用这个方法相当于与服务器建立连接打电话
socket=new Socket(serverIP,serverPort);
}
public void start(){
System.out.println("与服务器连接成功");
Scanner scanner=new Scanner(System.in);
try (InputStream inputStream=socket.getInputStream()){
try (OutputStream outputStream=socket.getOutputStream()){
while (true){
//仍然是四个步骤,从控制台获取子串,根据子串构造请求,再读取响应,解析,最后把结果显示到控制台
//1.控制台读取子串
System.out.print("->");
String request=scanner.next();
//2.根据子串构造请求
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();//不刷新可能不能很快得到数据
//.3.从服务器读取响应
Scanner respScanner=new Scanner(inputStream);
String response=respScanner.next();
//4.把结果写给控制台
System.out.printf("req: %s , resp: %s\n",request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TCPEchoClient tcpEchoClient=new TCPEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}
这边的发送和接收都使用printwrite包裹了一下
把请求和回复都包裹在outstream里
将request写进去
并用flush刷新