一.网络初识
链接: https://pan.baidu.com/s/1WspOI-oXB-WwE03z3BMU7Q 提取码: tyke
二.网络编程
1.网络编程基础
⽹络资源:就是在⽹络中可以获取的各种数据资源。
⽽所有的⽹络资源,都是通过⽹络编程来进⾏数据传输的。
网络编程:指⽹络上的主机通过不同的进程以编程的⽅式实现⽹络通信(或称为⽹络数据传输)
当然,我们只要满⾜进程不同就⾏;所以即便是同⼀个主机,只要是不同进程,基于⽹络来传输数 据,也属于⽹络编程。
特殊的,对于开发来说,在条件有限的情况下,⼀般也都是在⼀个主机中运⾏多个进程来完成⽹络编 程。 但是我们⼀定要明确,我们的⽬的是提供⽹络上不同主机,基于⽹络来传输数据资源
- 进程A:编程来获取⽹络资源
- 进程B:编程来提供⽹络资源
发送端和接收端
在⼀次⽹络数据传输时:
- 发送端:数据的发送⽅进程,称为发送端。发送端主机即⽹络通信中的源主机。
- 接收端:数据的接收⽅进程,称为接收端。接收端主机即⽹络通信中的目的主机。
- 收发端:发送端和接收端两端,也简称为收发端。
注意:发送端和接收端只是相对的,只是⼀次⽹络数据传输产⽣数据流向后的概念。
请求和响应
⼀般来说,获取⼀个⽹络资源,涉及到两次⽹络数据传输:
- 第⼀次:请求数据的发送
- 第⼆次:响应数据的发送。
好⽐在快餐店点⼀份炒饭: 先要发起请求:点⼀份炒饭,再有快餐店提供的对应响应:提供⼀份炒饭。
客⼾端和服务端
- 服务端:在常⻅的⽹络数据传输场景下,把提供服务的⼀⽅进程称为服务端,可以提供对外服务。
- 客⼾端:获取服务的⼀⽅进程,称为客⼾端。
对于服务来说,⼀般是提供:
- 客⼾端获取服务资源
- 客⼾端保存资源在服务端
常⻅的客⼾端服务端模型
客⼾端是指给⽤⼾使⽤的程序,服务端是提供⽤⼾服务的程序:
- 客⼾端先发送请求到服务端
- 服务端根据请求数据,执⾏相应的业务处理
- 服务端返回响应:发送业务处理结果
- 客⼾端根据响应数据,展⽰处理结果(展⽰获取的资源,或提⽰保存资源的处理结果)
2.Socket套接字
(1)概念和分类
Socket套接字是由系统提供⽤于⽹络通信的技术,是基于TCP/IP协议的⽹络通信的基本操作单元。基于Socket套接字的⽹络程序开发就是⽹络编程。
Socket套接字主要针对传输层协议划分为如下三类:
流套接字:使⽤传输层TCP协议 TCP,即TransmissionControlProtocol(传输控制协议),传输层协议。
以下为TCP的特点: • 有连接 • 可靠传输 • ⾯向字节流 • 有接收缓冲区,也有发送缓冲区 • ⼤⼩不限:对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是⽆边界的数据,可以多次发送,也可以分开多次接收。
数据报套接字:使⽤传输层UDP协议 UDP,即UserDatagramProtocol(⽤⼾数据报协议),传输层协议。
以下为UDP的特点: • ⽆连接 • 不可靠传输 • ⾯向数据报 • 有接收缓冲区,⽆发送缓冲区 • ⼤⼩受限:⼀次最多传输64k 对于数据报来说,可以简单的理解为,传输数据是⼀块⼀块的,发送⼀块数据假如100个字节,必须⼀次发送,接收也必须⼀次接收100个字节,⽽不能分100次,每次接收1个字节。
原始套接字:原始套接字⽤于⾃定义传输层协议,⽤于读写内核没有处理的IP协议数据。
(2)Java数据报套接字通信模型
对于UDP协议来说,具有⽆连接,⾯向数据报的特征,即每次都是没有建⽴连接,并且⼀次发送全部 数据报,⼀次接收全部的数据报。
以下只是⼀次发送端的UDP数据报发送,及接收端的数据报接收,并没有返回的数据。
对于⼀个服务端来说,重要的是提供多个客⼾端的请求处理及响应,流程如下:
(3)Java流套接字通信模型
(4)Socket编程注意事项
1. 客⼾端和服务端:开发时,经常是基于⼀个主机开启两个进程作为客⼾端和服务端,但真实的场 景,⼀般都是不同主机。
2. 注意⽬的IP和⽬的端⼝号,标识了⼀次数据传输时要发送数据的终点主机和进程
3. Socket编程我们是使⽤流套接字和数据报套接字,基于传输层的TCP或UDP协议,但应⽤层协议, 也需要考虑,这块我们在后续来说明如何设计应⽤层协议。
4. 关于端⼝被占⽤的问题
5. 如果⼀个进程A已经绑定了⼀个端⼝,再启动⼀个进程B绑定该端⼝,就会报错,这种情况也叫端⼝ 被占⽤。对于java进程来说,端⼝被占⽤的常⻅报错信息如下:
此时需要检查进程B绑定的是哪个端⼝,再查看该端⼝被哪个进程占⽤。以下为通过端⼝号查进程的⽅式:
在cmd输⼊netstat -ano | findstr端⼝号 ,则可以显⽰对应进程的pid。如以下命令显⽰了8888进程的pid
在任务管理器中,通过pid查找进程
解决端⼝被占⽤的问题:
- 如果占⽤端⼝的进程A不需要运⾏,就可以关闭A后,再启动需要绑定该端⼝的进程B
- 如果需要运⾏A进程,则可以修改进程B的绑定端⼝,换为其他没有使⽤的端⼝。
3.UDP数据报套接字编程
(1)API介绍
①DatagramSocket(⽤于发送和接收UDP数据报)
构造方法
方法
②DatagramPacket(Socket发送和接收的数据报)
构造方法
方法
③InetSocketAddress(( SocketAddress 的⼦类)构造⽅法)
(2)代码示例
①UDP Echo Server(UDP回显服务器)
public class UDPEchoServer { private DatagramSocket socket=null; public UDPEchoServer(int port) throws SocketException { this.socket=new DatagramSocket(port); //端口号 区分一个主机上的不同应用程序 } //启动服务器 public void start() throws IOException { System.out.println("server start"); while (true){ //1.读取请求并解析 此时requestPacket是receive的输出型参数 DatagramPacket requestPacket=new DatagramPacket(new byte[1024],1024); socket.receive(requestPacket); //此处把二进制数据转成字符串 String request=new String(requestPacket.getData(),0, requestPacket.getLength()); //2.根据请求计算响应 String response=process(request); //3.把响应返回给客户端 再去构造一个DatagramPacket DatagramPacket responsePacket=new DatagramPacket(response.getBytes(), response.getBytes().length,requestPacket.getSocketAddress()); socket.send(responsePacket); } } //由于当前是回显服务器 所以直接把request作为response返回 //如需编写其他服务器 只需修改process的逻辑 private String process(String request) { return request; } public static void main(String[] args) throws IOException { UDPEchoServer udpEchoServer=new UDPEchoServer(9121); udpEchoServer.start(); } }②UDP Echo Client(UDP客户端)
public class UDPEchoClient { //创建一个socket对象 private DatagramSocket socket=null; //服务器端的ip和端口号 String serverIp; int serverPort; //注意不用指定客户端的端口号 系统会自动分配! public UDPEchoClient(String serverIp, int serverPort) throws SocketException { this.socket=new DatagramSocket(); this.serverIp=serverIp; this.serverPort=serverPort; } public void start() throws IOException { System.out.println("client start"); Scanner scanner=new Scanner(System.in); //用户通过控制台输入字符串 把字符串发给服务器 从服务器读取响应 while (true){ //1.从控制台读取用户输入 System.out.println("->"); String request= scanner.next(); if(request.equals("exit")){ break; } //2.把用户输入的字符串包成一个数据报 并进行发送 DatagramPacket requestPacket=new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(this.serverIp),this.serverPort); socket.send(requestPacket); //3.从服务器读取响应 DatagramPacket reponsePacket=new DatagramPacket(new byte[1024],1024); socket.receive(reponsePacket); String reponse=new String(reponsePacket.getData(),0,reponsePacket.getLength()); //4.显示响应 System.out.println(reponse); } } public static void main(String[] args) throws IOException { UDPEchoClient udpEchoClient=new UDPEchoClient("127.0.0.1",9121); udpEchoClient.start(); } }③UDP Dict Server
编写⼀个英译汉的服务器.只需要重写process
public class UDPDictServer extends UDPEchoServer{ private Map<String,String> dict=new HashMap<String, String>(); public UDPDictServer(int port) throws SocketException { super(port); dict.put("hello","你好"); dict.put("world","世界"); dict.put("love","爱"); dict.put("file","文件"); //...... } @Override public String process(String request){ return dict.getOrDefault(request,"该单词不存在"); } public static void main(String[] args) throws IOException { UDPDictServer udpDictServer=new UDPDictServer(9101); udpDictServer.start(); } }4.TCP流套接字编程
(1)API介绍
①ServerSocket(创建TCP服务端Socket的API)
构造方法
方法
②Socket
Socket 是客⼾端Socket,或服务端中接收到客⼾端建⽴连接(accept⽅法)的请求后,返回的服 务端Socket。
不管是客⼾端还是服务端Socket,都是双⽅建⽴连接以后,保存的对端信息,及⽤来与对⽅收发数据的。
构造方法
方法
(2)代码示例
①TCP Echo Server
public class TCPEchoServer { private ServerSocket serverSocket=null; public TCPEchoServer(int port) throws IOException { this.serverSocket=new ServerSocket(port); } public void start() throws IOException { System.out.println("server start"); while (true){ //1.接受客户端的连接 //如果有客户端和服务器建立了链接 那么accept就可以返回 否则就会阻塞 Socket socket = serverSocket.accept(); //通过这个方法处理客户端的连接过程 processConnection(socket); } } private void processConnection(Socket socket) throws IOException { //在一次连接中 客户端和服务端可能会进行多组数据传输 InputStream inputStream=null; OutputStream outputStream=null; try { inputStream=socket.getInputStream(); outputStream=socket.getOutputStream(); Scanner scanner=new Scanner(inputStream); PrintWriter printWriter=new PrintWriter(outputStream); while (true){ //处理多次请求响应的读写操作 //1.读取响应并解析(使用Scanner) if(!scanner.hasNext()){ //客户端关闭连接 break; } String request=scanner.next(); //2.根据请求计算响应 String response=process(request); //3.把响应写回给客户端 printWriter.println(response); printWriter.flush();//把缓冲区的内容快速写入 } } catch (IOException e) { throw new RuntimeException(e); }finally { socket.close();//否则存在文件资源泄露问题 } } private String process(String request) { return request; } public static void main(String[] args) throws IOException { TCPEchoServer tcpEchoServer=new TCPEchoServer(9090); tcpEchoServer.start(); } }②TCP Echo Client
public class TCPEchoClient { private Socket socket=null; public TCPEchoClient(String socketIp,int socketPort) throws IOException { //客户端在new Socket时 就会和服务器建立tcp连接 socket=new Socket(socketIp,socketPort); } public void start() throws IOException { System.out.println("client start"); try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) { Scanner scanner=new Scanner(System.in); Scanner scannerNetwork = 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.从服务器读取响应 if(!scannerNetwork.hasNext()){ break; } String response=scannerNetwork.next(); //4.把响应显示到控制台 System.out.println(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(); } }③服务器引入多线程
默认情况下,IDEA 为了防止端口冲突,不允许同一个程序同时运行多个实例。
- 没勾选时:你启动第一个
TcpEchoClient后,再点运行按钮,会报错 “程序已经在运行”,无法启动第二个客户端。 - 勾选
Allow multiple instances后:你就可以同时启动多个TcpEchoClient,连接到你的服务器上,测试 “多客户端同时连接” 的场景。
public class TCPEchoServer { private ServerSocket serverSocket=null; public TCPEchoServer(int port) throws IOException { this.serverSocket=new ServerSocket(port); } public void start() throws IOException { System.out.println("server start"); while (true){ //1.接受客户端的连接 //如果有客户端和服务器建立了链接 那么accept就可以返回 否则就会阻塞 Socket socket = serverSocket.accept(); //通过这个方法处理客户端的连接过程 //不能直接调用processConnection方法 否则就会进入方法内部的while循环 //processConnection(socket); //此处应该创建新线程 在新线程里调用processConnection !!!!!!!!! Thread t=new Thread(()->{ try { this.processConnection(socket); } catch (IOException e) { throw new RuntimeException(e); } }); t.start(); } } private void processConnection(Socket socket) throws IOException { //在一次连接中 客户端和服务端可能会进行多组数据传输 InputStream inputStream=null; OutputStream outputStream=null; try { inputStream=socket.getInputStream(); outputStream=socket.getOutputStream(); Scanner scanner=new Scanner(inputStream); PrintWriter printWriter=new PrintWriter(outputStream); while (true){ //处理多次请求响应的读写操作 //1.读取响应并解析(使用Scanner) if(!scanner.hasNext()){ //客户端关闭连接 break; } String request=scanner.next(); //2.根据请求计算响应 String response=process(request); //3.把响应写回给客户端 printWriter.println(response); printWriter.flush();//把缓冲区的内容快速写入 } } catch (IOException e) { throw new RuntimeException(e); }finally { socket.close();//否则存在文件资源泄露问题 } } private String process(String request) { return request; } public static void main(String[] args) throws IOException { TCPEchoServer tcpEchoServer=new TCPEchoServer(9090); tcpEchoServer.start(); } }④服务器引入线程池
public class TCPEchoServer { private ServerSocket serverSocket=null; public TCPEchoServer(int port) throws IOException { this.serverSocket=new ServerSocket(port); } public void start() throws IOException { System.out.println("server start"); //创建线程池 不能使用FixedThreadPool(数目固定) ExecutorService executorService= Executors.newCachedThreadPool(); while (true){ //1.接受客户端的连接 //如果有客户端和服务器建立了链接 那么accept就可以返回 否则就会阻塞 Socket socket = serverSocket.accept(); //通过这个方法处理客户端的连接过程 //不能直接调用processConnection方法 否则就会进入方法内部的while循环 //processConnection(socket); //此处应该创建新线程 在新线程里调用processConnection // Thread t=new Thread(()->{ // try { // this.processConnection(socket); // } catch (IOException e) { // throw new RuntimeException(e); // } // }); // t.start(); //如果当前项目进一步增多 创建销毁进一步频繁 此时线程创建销毁的开销就不可忽视了 //引入线程池 executorService.submit(()->{ try { processConnection(socket); } catch (IOException e) { throw new RuntimeException(e); } }); } } private void processConnection(Socket socket) throws IOException { //在一次连接中 客户端和服务端可能会进行多组数据传输 InputStream inputStream=null; OutputStream outputStream=null; try { inputStream=socket.getInputStream(); outputStream=socket.getOutputStream(); Scanner scanner=new Scanner(inputStream); PrintWriter printWriter=new PrintWriter(outputStream); while (true){ //处理多次请求响应的读写操作 //1.读取响应并解析(使用Scanner) if(!scanner.hasNext()){ //客户端关闭连接 break; } String request=scanner.next(); //2.根据请求计算响应 String response=process(request); //3.把响应写回给客户端 printWriter.println(response); printWriter.flush();//把缓冲区的内容快速写入 } } catch (IOException e) { throw new RuntimeException(e); }finally { socket.close();//否则存在文件资源泄露问题 } } private String process(String request) { return request; } public static void main(String[] args) throws IOException { TCPEchoServer tcpEchoServer=new TCPEchoServer(9090); tcpEchoServer.start(); } }(3)长短连接
三.网络原理(计算机网络TCP/IP)
链接: https://pan.baidu.com/s/1RGxJgX2UpEoKnZsC39whHQ 提取码: xi42
:
异常情况详细:
3)异常关机
一、基础场景:无 NAT 参与的通信
家用设备之间(同局域网)
不涉及 NAT。
直接通过局域网内的不同 IP 地址区分设备并通信。
两个都带外网 IP 的设备之间
不涉及 NAT。
两个外网 IP 都是全局唯一的,可直接互相访问。
典型场景:服务器、运营商直连路由器自带外网 IP。
二、受限制的通信场景(网络层原生不允许)
不同局域网中的两个设备,尝试访问对方
网络层不允许直接访问。
举例:你电脑上跑 UDP 服务器,对方电脑用客户端无法直接访问。
但应用层可以通过技术手段实现(如 ToDesk 这类远程控制软件)。
外网 IP 设备,尝试主动访问内网 IP 设备
网络层不允许直接访问。
内网设备没有公网路由,无法被外网直接寻址。
三、NAT(网络地址转换)的触发场景
内网 IP 设备,主动访问外网 IP 设备
会触发NAT(网络地址转换)。
路由器 / NAT 设备会修改 IP 数据报的源 IP:
把内网设备的私有 IP 地址,替换成路由器的外网 IP 地址。
这样外网设备收到的请求,看起来就像是路由器发出来的。
注意:因为路由器的外网 IP 通常只有 1 个,但内网有很多设备同时上网,光靠 IP 区分不开。所以 NAT 会用「端口号」来区分不同的会话
(
- 你的电脑(内网 IP
192.168.1.100)访问百度,发出去的包会先到路由器。 - 路由器把包里的源 IP 和端口,改成自己的公网 IP 和一个临时端口,同时在「映射表」记下这个对应关系。
- 百度的响应包回来时,路由器再根据映射表,把目的 IP 和端口改回你的电脑地址,发给你。
)
| NAT 类型 | 转换方式 | 适用场景 |
|---|---|---|
| 静态 NAT | 内网 IP ↔ 外网 IP 一对一固定映射 | 企业服务器、需要固定公网 IP 的设备 |
| 动态 NAT | 内网 IP ↔ 外网 IP 动态池分配 | 早期企业网络,公网 IP 数量有限 |
| PAT(NAPT) | 内网 IP + 端口 ↔ 外网 IP + 端口 多对一映射 | 家用路由器、运营商宽带(最常见) |