核战争,简称“核战”,总括来说是属于使用核武器的战争 . 在核武器历史上,曾被使用的战争中只有在第二次世界大战中的广岛市原子弹爆炸、长崎市原子弹爆炸使用 . 在二十世纪六十年代 , 美苏争霸 ,古巴导弹危机,是1962年发生的事件,是冷战期间美国、苏联两国之间最激烈的一次对抗 . 而发射核弹 , 有三个重要部分 :
所以当时米国人就研究 , 能不能搞一个网络通信 , 不怕核弹的打击 , 即让整个通信链路变得复杂 , A->B之间有多个通信链路 !即使其中某些链路受到打击 , 剩下的链路仍能完成通信 .
这就演化成了互联网 , 虽然其没有起到预期的效果 , 但是基于其在传输数据方面发挥的重大作用 , 越来越多的计算机接入互联网 , 就构成了今天最大的互联网 -------Internet(因特网) . 而internet泛指互联网 ,即广义上的网络 .
随着时代的发展,越来越需要计算机之间互相通信,共享软件和数据,即以多个计算机协同工作来完成业务,就有了网络互连。
网络互连:将多台计算机连接在一起,完成数据共享。
数据共享本质是网络数据传输,即计算机之间通过网络来传输数据,也称为网络通信。
根据网络互连的规模不同,可以划分为局域网和广域网。
1.局域网
局域网,即 Local Area Network,简称LAN。
把多台机器连接到一起 , 就构成了局域网 .
局域网组建网络的方式有多种 :
两两相连的话 , 没那么多网线/网口 , 成本太高 ,所以就诞生了集线器 , 交换机和路由器 .
路由器:连接多个局域网;
2.广域网WAN
广域网,即 Wide Area Network,简称WAN .
在交换机和路由器的反复连接之下 , 就可以把很多很多设备都连接到一起 , 使它们可以进行通信 .
当机器足够多时 , 就可以成为"广域网" .
局域网和广域网 , 没有明确的界限 ! 广域网内部的局域网 , 都属于其子网 .
定义 : 网络协议为计算机网络中进行数据交换而建立的规则、标准或约定的集合。
协议其实就是"约定" , 通过一些约定 , 表达一定的含义 .
网络上传输的数据,本质上是光信号/电信号
不同的01排列组合,都要表达什么样的信息呢?都需要通过"协议"来约定!!!
网络通信非常复杂 , 如果使用一个协议来约定所有的问题 , 势必非常复杂 ; 所以大佬们就进行了程序拆分 , 把一个协议拆分成多个协议 . 拆着拆着 , 又发现许多协议其实解决了类似的问题 , 就把这些协议进行了分层 , 每一层内的诸多协议解决的问题都是类似的 .
Q : 为什么需要网络协议的分层 ?
A :
当前有两种分层模型.
参考资料 : 分层模型简介
物链网传会表用.
OSI:即Open System Interconnection,开放系统互连 .
OSI七层模型复杂且不实用 , 所以只是理论上存在 .
实际上实现的 , 是TCP/IP五层网络模型 .
只有应用层是程序猿自己写代码进行处理 , 其余各层都是由操作系统和硬件设备处理好的 .
网络编程的主要工作 , 就是写应用层的代码 , 来处理应用层的协议数据 .
理解网络层和数据链路层
比如我上学 , 要从沁源县到西安市 . 可以有这些路径 :
考虑走哪条路线 , 是网络层的工作 . (宏观) ,比如走第2条路线 .
考虑相邻节点之间具体怎么走 , 是数据链路层的工作. (微观) , 比如沁源—>临汾坐大巴车 , 临汾—>西安坐高铁 .
栗子:主机 A 通过微信发送 "hello" 给主机 B.
省略中间的传输过程 . 当主机B收到上述数据时 , 就是封装的逆过程 , 称为"分用" , 把每一层协议对应的报头给解析出来 , 并且去掉报头 . (拆快递) .
上述过程就体现出 , 网络通信中 , 各个层次的协议是如何配合工作的 .
上述的封装分用不仅出现在主机上 , 同样出现在传输过程中 ,包括在交换机和路由器上 .
用户在浏览器中,打开在线视频网站,如在B站观看视频,实质是通过网络,获取到网络上的一个视频资源 . 除了视频资源 , 网络上还有很多资源 , 如图片资源 , 文件资源 , 音乐资源…
所谓的网络资源,其实就是在网络中可以获取的各种数据资源。
而所有的网络资源,都是通过网络编程来进行数据传输的。
网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输).
通过写代码 , 实现两个/多个进程之间 , 通过网络进行相互通信 …
我们知道 , 进程具有隔离性 , 每个进程都有自己的独立虚拟地址空间 , 进程间通信 , 就是借助每个进程都能访问到的公共区域 , 实现数据的交换 . 网络编程借助的公共区域就是"网卡" . 通过这种方式 , 既可以让同一个主机的多个进程之间通信 , 也可以让不同主机的多个进程之间通信 .
网卡 :网卡是一块被设计用来允许计算机在计算机网络上进行通讯的计算机硬件。由于其拥有MAC地址,因此属于OSI模型的第1层和2层之间。它使得用户可以通过电缆或无线相互连接。
每一个网卡都有一个被称为MAC地址的独一无二的48位串行号,它被写在卡上的一块ROM中。在网络上的每一个计算机都必须拥有一个独一无二的MAC地址。
没有任何两块被生产出来的网卡拥有同样的地址。这是因为电气电子工程师协会(IEEE)负责为网络接口控制器(网卡)销售商分配唯一的MAC地址。
注意 : 发送端和接收端是相对的 , 只是一次数据传输过程中 , 发数据的那端就叫发送端了 .
服务器无法知道客户端什么时候来 , 所以服务器会长时间运行, 甚至7*24运行 .
进行网络编程 , 需要使用操作系统提供的网络编程API .
应用层要想进行网络编程的相关操作 , 就要调用传输层提供的一些功能 . 传输层就提供了网络通信的API , 这些API叫Socket API .
传输层提供了两个非常重要的协议 , 而且截然不同 . 他们就是TCP和UDP. 这两个协议对应的Socket api 也是截然不同的 .
TCP和UDP的特点
面向字节流:
创建一个TCP的socket,会在网络中同时创建一个发送缓冲区和接收缓冲区。
刚开始会将数据写入发送缓冲区。若数据太短,则在发送缓冲区中等待,等到合适时机会将合适大小的数据以字节流的形式发送出去;若数据太长,则进行拆分,然后发送。
由于TCP是全双工的,所以读写数据时没有限制,可以一次性接收所有数据;也可以每次接收一点,分多次接收。
TCP面向字节流的特点是:传输灵活,但是存在粘包问题。
Q : 什么是粘包问题 ?
A :
发送端为了将多个发往接收端的包 , 更有效的发给对方 , 使用了优化方法(Nagle算法),将多次间隔较小 , 数据量较小的数据包,合并成一个大的数据包发送(把发送端的缓冲区填满一次性发送)。从接收缓冲区看 , 后一个包的头连接着其前一个包的尾 , 好像粘在了一起 , 这就是"粘包" .
Q : 造成TCP粘包的原因 ?
A :
(1)发送方原因
TCP默认使用Nagle算法(主要作用:减少网络中报文段的数量),而Nagle算法主要做两件事:
Nagle算法造成了发送方可能会出现粘包问题 .
(2)接收方原因
TCP接收到数据包时,并不会马上交到应用层进行处理,或者说应用层并不会立即处理。
实际上,TCP将接收到的数据包保存在接收缓存里,然后应用程序主动从缓存读取收到的分组。这样一来,如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。
Q : 什么是丢包问题 ?
A :
udp是放在数据帧里面传输的 , 数据帧最多放1500个字节,除去udp的报头 一个数据帧最多发送1472个字节的udp , 如果udp过大 , 就会被拆分成多个数据帧发送到网络 , 即会造成丢包的现象。
Q : 为什么UDP不会粘包 ?
A :
TCP协议是面向流的协议,UDP是面向消息的协议 , 系统不会缓冲也不会优化 . UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据 .
其次 , TCP只要保证自己写入的流是按 长度 + 内容 + 长度 + 内容 , 这样就可以非常简单的解决粘包问题 .
参考资料 : TCP粘包与UDP丢包的原因
DatagramSocket的方法
DatagramPacket的构造方法
DatagramPacket的方法
写一个UDP版本的回显服务器-客户端.
注意 : 客户端发啥 , 服务器就返回啥, 不涉及任何业务逻辑 , 仅单纯演示API的用法 .
import javax.sql.DataSource;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
//要想创建UDP服务器,首先要打开一个socket文件
private DatagramSocket socket = null;
//提供构造方法,为该进程分配一个端口号
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
//启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
//服务器通常需要长时间工作,所以使用while(true)
while(true){
//1.读取客户端发来的请求
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
//2.对请求进行解析,把DatagramPacket转成一个String
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//3.根据请求,处理响应.
String response = process(request);
//4.把响应构造成DatagramPacket对象.
//构造响应对象,要搞清楚,对象要发给谁!谁发送的请求,就把响应发给谁.
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
//5.把这个DatagramPacket对象返回给客户端
socket.send(responsePacket);
System.out.printf("[%s:%d] req=%s ;resp=%s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),
request,response);
}
}
// 通过这个方法, 实现根据请求计算响应 这个过程.
// 由于是回显服务器, 所以不涉及到其他逻辑.
// 但是如果是其他服务器, 就可以在 process 里面, 来加上一些其他逻辑的处理.
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(8000);
server.start();
}
}
import java.awt.dnd.DropTarget;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
//客户端端口号,一般由操作系统自动分配
public UdpEchoClient() throws SocketException {
socket = new DatagramSocket();
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while(true) {
//1.客户端从控制台读取一个请求数据
System.out.println("> ");
String request = scanner.next();
//2.字符串发送给服务器,构造DatagramPacket
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName("127.0.0.1"), 8000);
//3.把数据报发给服务器
socket.send(requestPacket);
//4.从服务器读取响应数据
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
//5.获取响应数据,转成字符串
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.printf("req:%s ; resp:%s\n", request, response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient();
client.start();
}
}
代码中使用到的三种构造DatagramPacket的方法总结 :
其中1 , .用于接收数据 , 其余方法均用于发送数据 .
一个服务器 , 是可以给多个客户端提供服务的 . 需要对IDEA进行设置 .
同时启动两个客户端 :
这个容易处理 , 直接继承UdpEchoServer服务器 , 重写其中的process()方法即可 ! 这就是说 , process()方法不能设计成private类型的 .
例如实现一个具有英译汉功能的服务器 .
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class UdpDictServer extends UdpEchoServer {
private Map<String, String> dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
dict.put("cat","猫");
dict.put("apple","苹果");
dict.put("dog","狗");
}
public String process(String req){
return dict.getOrDefault(req,"别来沾边儿");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(8000);
server.start();
}
}
当我们发现某个端口,被其他进程占用了,导致咱们的服务器起不来.
通过netstat命令来找到是那个进程占用的.
netstat -ano | findstr “8000” 就能找到对应的pid(端口号)了.
ServerSocket的构造方法
ServerSocket的方法
Socket的构造方法
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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("服务器启动!");
ExecutorService service = Executors.newCachedThreadPool();
while(true) {
//.如果当前没有客户端来建立连接,accept会阻塞等待
Socket clientSocket = serverSocket.accept();
//1.单线程版本,只能处理单个客户端
//processConnect(clientSocket);
//2.多线程版本,主线程负责拉客,新线程负责通信
//.较版本一有提升,但是涉及到频繁创建销毁线程,在高并发的情况下,负担比较重.
// Thread t = new Thread(()->{
// try {
// processConnect(clientSocket);
// } catch (IOException e) {
// e.printStackTrace();
// }
// });
// t.start();
//3.线程池版本,解决了频繁创建销毁线程的问题
service.submit(new Runnable(){
@Override
public void run() {
try {
processConnect(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
/**
* 通过这个方法,给当前的这个客户端,提供服务!
* 一个连接过来了,服务方式可能有两种:
* 1.一个连接只进行一次数据交互(一个请求,一个响应) 短连接
* 2.一个连接只进行多次数据交互(N个请求,N个响应) 长连接
* //此处是长连接版本
* @param clientSocket
*/
public void processConnect(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 建立连接!\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
//长连接,通过循环来实现多次交互!
while(true) {
if(!scanner.hasNext()){
//.连接断开,当客户端连接断开,hasNext会返回false
System.out.printf("[%s:%d]断开连接!\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
break;
}
//1.读取请求并解析
String request = scanner.next();
//2.根据请求计算响应
String response = process(request);
//3.把响应写回给客户端
printWriter.println(response);
//4.刷新缓冲区
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 {
clientSocket.close();
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(8000);
server.start();
}
}
上述分析针对单线程版本 , 只能处理一个客户端 . 为什么呢 ?
不要误以为TCP服务器必须使用多线程处理 !
TCP服务器这里之所以使用了多线程,是因为在代码中处理了"长连接".客户端建立好连接后,不确定什么时候断开连接,也不确定一个连接中要处理的请求数量(因为是长连接),所以就导致在处理连接的代码处循环,主循环就无法执行到accept了.如果代码仅用于处理短连接,即每次客户端一个连接只处理一个请求,不使用多线程也能处理多个客户端.
上述版本二中 , 可以同时处理多个服务器了 , 但是涉及到频繁创建销毁线程 , 在高并发的情况下 , 负担比较重 .
解决方案 : 版本三,线程池!
拓展 :
线程池可以解决频繁创建销毁线程的问题 . 但如果并法量太高 , 就会导致池子里的线程特别多 ,造成内存等资源的过度开销 .
进一步考虑 , 能否减少线程的数目呢 ? 当前是一个线程对应一个客户端 , 能否让一个线程对应多个客户端呢 ?
这就是IO多路复用 , 本质上是一个线程处理多个socket . 在java封装后 , 是NIO .
关于NIO , 可以参考下文 , 此处不进行展开 .
什么是NIO ? NIO的原理是什么 ?
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient() throws IOException {
socket = new Socket("127.0.0.1",8000);
}
public void start(){
//长连接,一个连接会处理N个请求和响应
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
Scanner scannerNet = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while(true){
//1.从控制台读取用户的输入
System.out.println("> ");
String request = scanner.next();
//2.把请求发送给服务器
printWriter.println(request);
printWriter.flush();
//3.从服务器读取响应
String response = scannerNet.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 client = new TcpEchoClient();
client.start();
}
}
Q : 在服务器的代码中 , 我们使用了hasNext()判断是否有响应中断 , 而在客户端代码中则没有这么做 .Why ?
A :
Q : 客户端只是用一个String接收响应 , 如果是一次请求 , 多次响应 ,怎么办 ?
A :
TCP的本质 : 字节流
如果服务器返回多次响应 , 这多次响应 , 都是以字节为单位返回给客户端的 .
客户端操作系统内核就会受到这些字节 , 之后调用read/scanner.next()这样的方法从内核中读取数据 .
例如使用read时 ,返回10次 , 每次返回10个字节 , 只需read(buf[100])就可以一次都读取出来了 …
而scanner.next()则是读到空白符返回 , 即空格,回车,换行,制表,翻页… ,返回值中不包含空白符!(无论最后有几个空白符 , 都不会被包含在返回值内!)
也就是说 , 不管服务器怎么返回 , 客户端这边是爱怎么取怎么取 ,一次取完也行 , 分多次取完也行 .
Q : 我在客户端代码中, 发送请求时使用了write而不是println ,; 在服务器代码中 , 返回响应也使用了write而不是println , 代码跑不起来 , 效果如下图 , 为什么 ?
输入内容 , 发现没响应 , 可能是 :
此时服务器还是在阻塞的状态 , 有可能是没有加flush(),但是我们的代码中是有的 .
如何确认阻塞在哪里呢 ? 可以使用jconsole查看当前进程的情况 , 可以看到每个进程的调用栈 .
通过TCP 的Socket API , 写一个翻译功能的服务器 .(仍使用原来的客户端代码 , 改变服务器代码即可)
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class TcpDictServer extends TcpEchoServer {
private Map<String,String> dict = new HashMap<>();
@Override
public String process(String request) {
return dict.getOrDefault(request,"别来沾边儿");
}
public TcpDictServer(int port) throws IOException {
super(port);
dict.put("cat","猫");
dict.put("dog","狗");
dict.put("apple","苹果");
}
public static void main(String[] args) throws IOException {
TcpDictServer server = new TcpDictServer(8000);
server.start();
}
}
本课内容结束 !