JavaEE--8.网络编程
一、基本介绍
1.1 局域网
局域网,即Local Area Network,简称LAN。
Local标识了局域网是本地,局部组建的一种私有网络
局域网内的主机之间能方便的进行网络通信,又称为内网;局域网和局域网之间
在没有连接的情况下是无法通信的,局域网组建网络的方式有很多种。
(1)基于网线直接连接
(2)基于集线器组建
(3)基于交换机组建
(4)基于交换机和路由器组建
交换机相当于是对一两千的端口进行了扩展
猫(调制解调器)不同种类的信号转换
猫的作用就是把电话线种的模拟型信号转换成网络的“数字信号”
1.2 广域网
广域网,即Wide Area Network,简称WAN
通过路由器将多个局域网连接起来,在物理上组成很大范围的网络,就形成了广域网。广域网的内部的局域网都属于其子网。
1.3 IP地址
描述了一台主机在互联网上的位置,IP地址是使用一个32位的整数表示的
使用“点分十进制”这样的方法来表示“IP地址”,这样方便阅读
点分十进制 即 a.b.c.d 的形式(a,b,c,d都是0~255之间的十进制整数)
就像这样的形式
1.4 端口号
区分当前主机上的指定的应用程序(进程)
一个主机上使用网络的程序有很多个,通过端口号区分,当前主机收到的数据是交给哪个程序来处理使用
端口号同样也是个整数,是一个2个字节的整数(0--->65535)
但是是0 -> 65535这样的范围,实际上0->1023这些端口都是有一些特定含义的
平时写代码用的都是剩下的端口
1.5 认识协议
网络中的中的核心概念,协议是进行一切通信的基础,通信至少需要两个主机
让发送方法的数据接收方能理解,双方需要按照同样的规则来构造/解析数据
协议就是约定通信双方交换数据的规则
协议最终体现在网络上传输的数据包格式
使用一个协议约定所有的网络通信细节,导致这个协议会十分庞大,十分复杂
所以会把这一个大的协议拆分成多个小的协议
让每一个小的协议,专注于解决一个/一类问题,再让这些协议相互配合
协议分层(把功能定位类似的协议放到一层里,并且约定好曾与层的关系)
协议之间不能随意交互,只有相邻的层才可以交互
上层协议调用下层协议,下层协议为上层协议提供服务
协议分层的好处:
1.降低了使用的成本,使用某个协议的时候不需要关注其他协议的实现细节
2.降低了整个体系的耦合性,灵活的变更某个层次的细节
咱们互联网体系的现状就是“协议分层”的效果
1.6 TCP/IP五层网络模型
当前的协议分层具体如何划分?
OSI七层网络模型
TCP/IP五层网络模型(当前世界上最主流的)
应用层
传输层
网络层
数据链路层
物理层
除了应用层,其他四层程序员干预不了,操作系统/硬件设备已经实现好了
传输层、网络层、数据链路层是数据转发的过程
应用层:负责应用程序之间的沟通。网络编程主要针对应用层
传输层:主要关注网络通信中的“起点和终点”,并不关心通信的中间细节
网络层:进行网络通信的路径规划和地址管理
数据链路层:在针对上述规划好的路径,进行具体的实施
物理层:描述的硬件设备需要满足什么样子条件,物理层相当于“公路”、“铁路”
TCP/IP中把表示层和会话层和应用层融合到一起了
网上有的资料称为“TCP/IP”四层模型,没有算上物理层,因为物理层和硬件相关,离程序员非常远
网络设备所在的分层
(1) 对于一台主机,它的操作系统内核实现了从传输层到物理层的内容,也就是TCP/IP五层模型的下四层
(2)对于一个路由器,它实现了从网络层到物理层,也就是TCP/IP五层模型的下三层
(3)对于一个交换机,它实现了从数据链路层到物理层,也就是TCP/IP五层模型的下两层
(4) 对于一个集线器,它实现了物理层
所以我们称为三层路由器,两层交换机
1.7 封装和分用
通过QQ把 hello 给另一个人
【站在发送方的视角】
封装(计算机网络中的封装其实是字符串的拼接)
1.用户输入框中输入“hello”字符串,点击发送按钮
qq这样的程序就会把hello这个内容从输入框中读取,构造成一个“应用层数据包”
应用层的网络协议就描述了这个数据包的构造,此处的应用层协议往往是开发这个qq的程序员自行定义的
假设一种定义结构(定义的方式有很多种)
数据包的格式:发送者的qq号;接受者的qq号;发送的时间;消息正文\n
数据报的样例:123456789;987654321;2025-09-01 12:00;hello\n
2.qq这样的应用程序会调用操作系统提供的API(传输层给应用层提供的api)
操作系统就会提供一个类似于“发送数据”这样的api,然后应用程序就会把上述组织好的应用程序数据包作为参数传进来,于是应用程序数据包就到了系统内核里,进入了传输层的代码部分
此时传输层这里就会把上述的应用层数据再进一步的封装成一个传输层数据包
由于传输层有多种协议(其中最主要的是两个,TCP/UDP)
假设此处使用的是UPD协议(这两个协议给应用层提供的api是两组不同的,看应用程序的代码使用的是哪个api)
UDP报头包含了一些UDP相关的信息,比如发送者的端口号和接受者的端口号
3.传输层构造好数据之后,就会继续调用网络层提供给传输层的api,进一步把数据交给网络层
由于传输层和网络层都是操作系统内核里面实现好的,上述调用过程咱们无需关心也感知不到
网络层也有多重协议,其中最主要的就是IPv4协议(简称IP协议)
IP协议会把上述拿到的传输层数据包构成网络层数据包
字符串拼接,再次拼接上IP报头
IP报头:包含很多信息,这里最主要的信息就是发送方的IP地址和接收方的IP地址
4.网络层继续调用数据链路层的api,把数据交给数据链路层处理
数据链路层的常见协议 以太网(平时插网线,进行上网的这种方式)
在ip数据包的基础上再进行进一步的包装,加上以太网数据帧(帧头和帧尾)
5.在上述得到的数据,需要进一步交给物理层(硬件设备)
网卡就会针对上述的二进制数据进行真正的传输操作
就会把上述的0101这种序列转化为光信号/电信号/电磁波
分用
【接收方的视角】
1.接收方物理层收到光电信号
把这样的光电信号还原成0101这样的二进制字符串
2.物理层转换回来的数据交给数据链路层
以太网拿到这个数据包,就会对这个包进行解析
拿出这里的报头和载荷,根据报头重的信息做一些处理
决定这个数据包是丢弃还是转发还是自己保留(向上解析)
3.网络层拿到上述解析好的数据
IP协议也要对这个数据包进行解析,取出IP报头和载荷
也是根据报头等信息确认(丢弃,转发,还是保留(给上层协议))
4.传输层这边,UDP协议也要针对数据进行了解析,取出UDP报头和载荷
也是需要把载荷里面的内容进一步交给应用层协议(应用程序)
依赖UDP报头中的“端口号”,端口号就是用来区分不同的进程的
5.数据就到了qq这样的应用程序这里了
qq针对上述数据进行“反序列化”
进行网络传输需要把一个结构化的数据转化为一个字符串
序列化:结构化数据 => 二进制字符串
反序列化:二进制字符串 => 结构化数据
总结
1.不同的协议层对数据包有着不同的称谓,在传输层叫做段,在网络层叫做数据报,在链路层叫做帧
2.应用层数据通过协议栈发送到网络上时,每层协议都要加上一个数据首部,称为封装
3.首部信息重包含一些类似于首部有多长,载荷有多长,上层协议是什么等信息
4.数据封装成帧后发送到传输介质上,到达目的主机后每层协议再剥掉相应的首部,根据首部中的上层协议字段,将数据交给对应的上层协议处理
5.传输中间可能存在很多的交换机(二级转发)和路由器(三级转发),中间过程的交换机和路由器也会涉及到封装和分用,交换机分装分用到数据链路层,就可以决定数据是丢弃还是继续转发了,不再继续分用(经典的教科书上的交换机);路由器分装分用到网络层,就可以决定数据丢弃还是继续转发了,也不再继续分用
二、网络编程套接字
套接字是socket的意思
操作系统给应用程序(传输层给应用层)提供的api起了个名字,叫做socket
socket本身就是“插槽”的意思
接下来学习的就是操作系统提供的socket.api(Java版本)
socket.api提供了两组不同的api
UDP有一套
TCP也有一套
两个差别有点大,不得不搞两套
1.UDP/TCP的区别
UDP/TCP的区别
TCP 有连接 可靠传输 面向字节流 全双工
UDP 无连接 不可靠传输 面向数据报 全双工
有连接/无连接
此处的连接是抽象的连接,通信双方如果保存了通信对端的信息,就是相当于是“有连接”
如果不保存对端的信息,就是无连接
可靠传输/不可靠传输
此处谈到的“可靠”不是指100%能到达对方,而是尽可能
相对来说,不可靠就是完全不考虑数据是否能够到达对方
TCP内置了一些机制,能够保证可靠传输
1)感知对方是不是收到
2)重传机制,在对方没收到的时候尝试重试
UDP则没有可靠性机制,完全不管发出去的数据是否顺利到达对方
可靠性需要付出代价,TCP协议设计的比UDP复杂的多,也会损失传输数据的效率
面向字节流/面向数据报
TCP是面向字节流的
TCP的传输过程就是和文件流/水流是一样的特点,传输固定的数据可以分次进行传输
UDP面向数据报
传输数据的基本单位不是字节,而是“UDP数据报”,
一次发送/接受,必须发送/接受完整的UDP数据报
全双工/半双工
全双工:一个通信链路可以发送数据,也可以接受数据(双向通信)
半双工:一个通信链路,只能发送/接受(单向通信)
2.UDP数据报套接字编程
DatagramSocket是UDP socket,用来发送和接受UDP数据报
代表一个socket对象
通过代码直接操作网卡不好操作(网卡有很多种不同的型号,之间提供的api都会有差别)
操作系统就把网卡概念封装成socket,又要程序员不必关心硬件的差异和细节,统一去操作socket对象,就能间接的操作网卡
socket可以认为操作系统中广义下的文件里的一种文件类型,这样的文件就是网卡这种硬件设备的抽象表示形式
DatagramSocket构造方法
方法签名 | 构造方法 |
DatagramSocket() | 创建⼀个UDP数据报套接字的Socket,绑定到本机任意⼀个随机端口(⼀般用于客户端) |
DatagramSocket(int port) | 创建⼀个UDP数据报套接字的Socket,绑定到本机指定的端口(⼀般用于服务端) |
DatagramSocket方法
方法签名 | 方法说明 |
void receive(DatagramPacket p ) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacket p) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
DatagramPacket
DatagramPacket是UDP Socket发送和接受的数据报
DatagramPacket构造方法:
方法签名 | 方法说明 |
DatagramPacket(byte[] buf, int length) | 构造⼀个DatagramPacket以⽤来接收数据报,接收的 数据保存在字节数组(第⼀个参数buf)中,接收指定 ⻓度(第⼆个参数length) |
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) | 构造⼀个DatagramPacket以⽤来发送数据报,发送的数据为字节数组(第⼀个参数buf)中,从0到指定⻓度(第⼆个参数length)。address指定⽬的主机的IP 和端⼝号 |
DatagramPacket方法:
方法签名 | 方法说明 |
InetAddress getAdd() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端⼝号;或从发送的数据报中,获取接收端主机端⼝号 |
byte[] getData() | 获取数据包中的数据 |
代码部分需要实现两个程序
1)UDP服务器
2)UDP客户端
网络通信中,主动发起通信的一方就是客户端,被动接受的一方就是服务器
使用DataagramSocket和DatagramPacket实现一个“回显服务器”
回显服务器(echo server):
客户端发啥样的请求,服务器就返回啥样的响应
没有任何业务逻辑,进行任何计算或者处理
服务器的代码:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;public class UDPEchoServer {private DatagramSocket socket= null;public UDPEchoServer(int port) throws SocketException {socket=new DatagramSocket(port);}//通过start启动服务器的核心流程public void start() throws IOException {System.out.println("服务器启动");//此处通过死循环不停的处理客户端的请求while(true){//1.读取客户端的请求并且解析DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);socket.receive(requestPacket);//上述收到的数据是二进制byte[]形式体现的,后续代码如果需要打印之类的处理操作//需要转成字符串才好处理String request=new String(requestPacket.getData(),0,requestPacket.getLength());//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] request=%s response=%s",requestPacket.getAddress(),responsePacket.getPort(),request,response);}}private String process(String request) {return request;}public static void main(String[] args) throws IOException {UDPEchoServer udpEchoClient=new UDPEchoServer(9090);udpEchoClient.start();}
}
网络编程必须有网卡,就需要socket对象
对于服务器这一段,需要在socket对象创建的时候,就指定一个端口号,作为构造方法的参数
后续服务器开始运行之后,操作系统就会把端口号和该进程关联起来
此时会抛出一个异常,SocketException,是IOException的子类
在调用这个构造方法的过程中,jvm会调用系统的socket api 完成端口号-进程的关联
“绑定端口号”(原生系统的api取得名字就是bind(绑定的意思))
对于一个系统来说,同一个协议下,应该端口号只能被一个进程绑定
但是对于一个进程来说可以绑定多个端口号(需要创建多个socket对象来完成)
端口号就是为了区分进程,收到数据之后明确说明这个数据交给谁
1.读取客户端的请求并且解析
这里需要创建一个数据报,里面放空的数组
然后调用receive来将从网卡上接收到的数据存储到这个数据报中,此时这个参数就是输出型参数
如果网卡上没有收到数据,receive就会阻塞等待,一直等到真正的收到数据为止
创建一个字符串从数据报里得到数组,转化为字符串 从0 开始,
大小为requestPacket.getLength()
2.根据请求计算相应,由于此处是回显服务器,响应就是请求
//3.把响应写会到客户端
针对响应的返回,不能使用空的数组,所以需要用process来计算返回结果
创建一个数据报,将字符串转化为数组,参数还有大小,以及发出请求的端口号和IP
这里需要注意的是可能会写成 response.geLength()这里是这个字符串的字符数,
这里是需要的是字节数的个数
UDP有一个特点就是无连接,所谓的链接就是通信双方保存对方的信息(IP+端口)
DatagramSocket这个对象中不持有对方(客户端)和ip和端口
进行send 的时候就需要在send的数据报中把要发给谁的信息写进去,才能正确的把数据进行返回
相比之下TCP的代码中就不需要关心对端的ip
使用完毕后要关闭
此处代码中,socket生命周期应该是跟随整个进程的
进程结束了,socket才需要关闭,此时就算代码没有close,
进程关闭也会释放文件描述符表里的所有内容, 也就相当于close 了
服务器这边,创建socket 一定要指定端口号
服务器必须要指定端口号,客户端主动发起的时候才能找到服务器
客户端这边创建socket最好不要指定端口号
(不指定不代表没有,客户端这边的端口号是系统自动分配的)
主要是如果端口号已经被其他进程占用了,用户就不知道如何处理了
客户端这里需要对端的目标ip和端口号
UDP客户端的代码
port java.io.IOException;
import java.net.*;
import java.util.Scanner;public class UDPEchoClient {private String serverIp;private int serverPort;private DatagramSocket socket=null;public UDPEchoClient(String ip,int port) throws SocketException {this.serverIp=ip;//对端IPthis.serverPort=port;//对端端口号socket=new DatagramSocket();//客户端不指定端口号,服务器指定端口号}public void start() throws IOException {System.out.println("启动客户端");Scanner scanner=new Scanner(System.in);while(true) {//1.启动服务器,获取输入的内容System.out.print("请输入请求内容->");String request = scanner.next();//2.构造出一个UDP请求,发送给服务器DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(this.serverIp), this.serverPort);socket.send(requestPacket);//3.从服务器中读取响应DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);socket.receive(responsePacket);//4.把响应打到控制台上String response = new String(responsePacket.getData(), 0, responsePacket.getLength());System.out.println(response);}}public static void main(String[] args) throws IOException {UDPEchoClient udpEchoClient=new UDPEchoClient("127.0.0.1",9090);udpEchoClient.start();}
}
此处是给服务器发送数据,发送数据的时候,UDP数据报就要自动需要带有目标的IP和端口
接受数据的时候,构造的UDP是一个空的数据报
这里的“127.0.0.1”
这个是特殊的IP,环回IP
这个IP代表本机,如果客户端和服务器在同一个主机上,就使用这个IP
在客户端中
客户端输入内容之后,会发送请求
发送完毕之后会继续往下走直到receive这里阻塞,等待服务器的响应
得到服务器的返回的响应之后,就会在上面的receive解除阻塞后继续执行
打印完日志后也进入了下一次的循环
在服务器中
由于客户端没有发送请求,这是receive就会阻塞,直到请求发送
收到请求后就往下走,直到send发送完毕打印日志后进入下一轮循环
然后继续在receive中等待
网络编程的意义:只要能连上网络就可以通过网络和服务器进行交互,哪怕在世界上任何的角落
3.TCP流套接字编程
API介绍
ServerSocket:是创建TCP服务器Socket的API
ServerSocket构造方法:
方法签名 | 方法说明 |
serverSocket ( int port) | 创建一个服务器流套接字Socket,并绑定到指定的端口 |
ServerSocket的方法
方法签名 | 方法说明 |
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客客端 连接后,返回⼀个服务端Socket对象,并基于该 Socket建⽴与客户端的连接,否则阻塞等待 |
void close | 关闭此套接字 |
Socket类
Socket 是客⼾端Socket,或服务端中接收到客⼾端建⽴连接(accept⽅法)的请求后,返回的服务端Socket。 不管是客⼾端还是服务端Socket,都是双⽅建⽴连接以后,保存的对端信息,及⽤来与对⽅收发数据 的。
Socket的构造方法
方法签名 | 方法说明 |
Socket(String host,int port) | 创建⼀个客⼾端流套接字Socket,并与对应IP的主机 上,对应端⼝的进程建⽴连接 |
Socket方法
方法签名 | 方法说明 |
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
编写TCP回显服务器
服务器的代码
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;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){//多线程形式Thread thread=new Thread(()->{try {Socket clientSocket = serverSocket.accept();processConnection(clientSocket);} catch (IOException e) {e.printStackTrace();}});thread.start();}}//针对一个链接提供处理逻辑private void processConnection(Socket clientSocket) {//先打印客户端信息System.out.println("客户端上线!!!");//获取socket中持有的流对象try(InputStream inputStream=clientSocket.getInputStream();OutputStream outputStream=clientSocket.getOutputStream()){Scanner scanner=new Scanner(inputStream);PrintWriter printWriter=new PrintWriter(outputStream);//使用scanner包装一下inputStream吗,就可以方便的读取这里的请求数据了while(true){//1.读取请求并解析if(!scanner.hasNext()){//没有数据可读的时候返回true,连接断开返回falsebreak;//连接打开没有数据到来的时候阻塞}String request=scanner.next();//2.根据请求计算响应String response=process(request);//3.把响应写回给客户端printWriter.println(response);//将数据写回给客户端printWriter.flush();//将缓冲区的数据冲刷下//打印日志System.out.printf("[%s:%d] resquesr=%s response=%s",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);}}catch (IOException e){e.printStackTrace();}finally {System.out.printf("【%s:%d】客户端下线!!!\n",clientSocket.getInetAddress(),clientSocket.getPort());}}private String process(String request) {//回显服务器return request;}public static void main(String[] args) throws IOException {TCPEchoServer server=new TCPEchoServer(9090);server.start();}
}
TCP在建立连接的流程是操作系统内核完成的,咱们的代码是感知不到的
accept操作是内核已经完成了连接建立的操作,然后才能进行“接通电话"
accept相当于是针对内核中已经建立好的连接进行“确认”动作
ServerSocket 和Socket都是socket,都是网卡的控制器,都是操作网卡的
但是在TCP中使用两个不同的socket进行表示,分工和作用是不同的
每一次服务器调用一次accept都会产生一个新的socket对象,来和客户端进行一对一服务器
TCP是全双工通信,一个socket对象既可以读,也可以写
在响应写回客户端的时候需要\n,这样在scanner.next()的读取的时候,读取的\n的时候就会停下来
在建立连接后,当读取到数据的时候scanner.hasNext(),返回true,前面有取反,所有读取有数据来之后跳过这个判断
当断开连接的时候返回false,前面有取反,所以直接进入break
当连接已经建立,并且没有数据的时候,会进行阻塞
快捷键介绍:在一个代码外套一个代码(try,if,while,for),这个功能是surround功能
主流的开发工具基本都有
选中要被套的内容后,按下ctrl+alt+t
服务器中的阻塞有两处
第一处:
服务器的第一处阻塞等待客户端连接(new Socket)
第二处:
服务器的第二处阻塞
在等待客户端发送数据之前进行组合(客户端println)
TCP客户端代码:
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(String ip,int port) throws IOException {socket=new Socket(ip,port);}public void start() throws IOException {System.out.println("客户端启动!");try(OutputStream outputStream=socket.getOutputStream();InputStream inputStream=socket.getInputStream()){Scanner scanner=new Scanner(inputStream);Scanner scannerIn=new Scanner(System.in);PrintWriter printWriter=new PrintWriter(outputStream);while(true){//1.从控制台读取数据System.out.println("输入请求内容");String request= scannerIn.next();//2.把请求发送给服务器printWriter.println(request);printWriter.flush();//3.从服务器中读取响应while(!scanner.hasNext()){break;}String response=scanner.next();//4.打印响应结果System.out.println(response);}}catch (IOException e){e.printStackTrace();}finally {socket.close();}}public static void main(String[] args) throws IOException {TCPEchoClient client=new TCPEchoClient("127.0.0.1",9090);client.start();}}
在写的过程中可能会存在的问题
1.可能会发现请求或者响应无法正常发送
解答:PrintWriter这样的类 以及许多IO流中的类都是“自带缓冲区”的
引入缓冲区之后,进行写入操作不会立即触发IO,而是先放到内存缓存区中
等到缓冲区攒了一波后进行统一的发送
在这里也就是把请求先放进了缓冲区里,由于此处的数据比较少,没有办法一次性发送
因此这样的数据就一直停留在缓冲区中
解决办法:增加printWriter.flush()方法,进行冲刷缓冲区
未来在很容易遇见这种问题,比如添加日志的过程中,打印日志的函数已经执行到了就是没有日志出来,可能是缓冲区导致的
2.当前的服务器代码针对客户端没有进行close操作
像ServerSocket DatagramSocket这种生命周期是跟随整个进程的
这里的clientSocket是连接级别的数据
随着客户端断开连接了,这个socket就再也不适用了(即使是同一个客户端,断开之后,重新连接也是一个新的socket,和旧的socket不是同一个)
因此,这样的socket就应该被主动关闭掉,避免文件资源泄露
GC=garbage Collection 垃圾回收机制
GC释放的是内存资源,这里的文件资源泄露针对的是文件描述符表
流对象虽然被GC回收了,也会自动执行close的
但是gc 的回收过程是不可控的(不知道何时发生,也不知道这次的gc是否能释放掉你的这个对象)
因此不能全部指望,所以养成良好的习惯,有创建就有释放
3.尝试使用多个客户端来同时连接服务器
可以使用多线程,来解决多个客户端来同时连接服务器
这样的代码属于比较经典的一种服务器开发的模型,给每个客户端来提供服务
但是短时间有大量的客户端,并且每个客户端的请求很快
这个时候对服务器来说,有比较大的压力
虽然创建线程比创建进程更轻量,但是也架不住短时间创建销毁大量的线程
例如:直播中的弹幕
短时间内有创建大量的客户端
1.客户端发来一个请求后快速断开了连接(可以使用线程池来解决这个问题)
线程池的本质上就是一个线程服务一个客户端,使用线程池就是在复用线程
此处的线程池的最大线程数是非常大的
可以看到,线程数最大值大约21亿
如果用的是固定线程数只能同时处理这么多客户端
2.客户端持续的发送请求处理响应,连接会保持很久
此时使用多线程/线程池都不合适
此时可以使用IO多路复用
虽然客户端数目非常多,依旧可以使用较小的线程提供高效的服务
这个已经被使用的各路框架封装在里面了,所以不需要过多了解
IO多路复用:
一个线程服务一个客户端,每个线程都可能会阻塞(客户端也不是持续的发送请求)
相比处理请求,大部分的时间可能都是在阻塞等待
如果让一个线程同时给多个客户端提供服务
比如:给一个线程分配1000个客户端进行处理,同一时刻可能有几十个客户端需要处理请求
针对这样的情况,操作系统提供了IO多路复用这个功能(IO多路复用的具体实现有很多,最知名的就是Linux下的epoll)
epoll就是在内核中搞了一个数据结构,可以把多个socket(每个socket对应一个客户端)放到这个数据结构里
同一时刻,大部分的socket都是处于阻塞等待的(没有数据需要处理)
少数收到数据的socket,epoll就会通过回调函数的方法通知应用程序这里有数据了
应用程序就会使用少量线程,针对这里的有数据的socket进行处理
一个进程中三个特殊的流对象(特殊的文件描述符表)
1.System.in===》标准输入
2.System.out===》标准输出
3.System.err===》标准错误
这里三个不能close,是进程一启动,操作系统自动打开的,生命周期是要跟随整个进程的
标准输出和标准错误都是显示在控制台上的,看起来没区别
但是标准输出,标准错误这些内容是支持“重定向的”,可以把这些输出的内容重定向到文件中
如果采用重定向的话,可以把标准输出和标准错误重定向到不同的文件中
一个是打印程序的正常的信息
一个是打印程序异常的信息
例如:e.printStackTrace() 打印异常的调用栈信息
长连接 :客户端连上服务器之后,一个连接中会多次发起请求,接受多个响应(一个连接到底进行多少次请求,不确定),当前echo client就是这样
短连接:客户端连接上服务器之后,一个连接只发一次请求,接受一次响应,然后断开连接
可能会频繁的和服务器建立/断开连接
建立断开连接也是有开销的
总结:
UDPEchoClient
import java.io.IOException;
import java.net.*;
import java.util.Scanner;public class UDPEchoClient {private String serverIp;private int serverPort;private DatagramSocket socket=null;public UDPEchoClient(String ip,int port) throws SocketException {this.serverIp=ip;//对端IPthis.serverPort=port;//对端端口号socket=new DatagramSocket();//客户端不指定端口号,服务器指定端口号}public void start() throws IOException {System.out.println("启动客户端");Scanner scanner=new Scanner(System.in);while(true) {//1.启动服务器,获取输入的内容System.out.print("请输入请求内容->");String request = scanner.next();//2.构造出一个UDP请求,发送给服务器DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,InetAddress.getByName(this.serverIp), this.serverPort);socket.send(requestPacket);//3.从服务器中读取响应DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);socket.receive(responsePacket);//4.把响应打到控制台上String response = new String(responsePacket.getData(), 0, responsePacket.getLength());System.out.println(response);}}public static void main(String[] args) throws IOException {UDPEchoClient udpEchoClient=new UDPEchoClient("127.0.0.1",9090);udpEchoClient.start();}
}
UDPEchoServer
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;public class UDPEchoServer {private DatagramSocket socket= null;public UDPEchoServer(int port) throws SocketException {socket=new DatagramSocket(port);}//通过start启动服务器的核心流程public void start() throws IOException {System.out.println("服务器启动");//此处通过死循环不停的处理客户端的请求while(true){//1.读取客户端的请求并且解析DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);socket.receive(requestPacket);//上述收到的数据是二进制byte[]形式体现的,后续代码如果需要打印之类的处理操作//需要转成字符串才好处理String request=new String(requestPacket.getData(),0,requestPacket.getLength());//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] request=%s response=%s",requestPacket.getAddress(),responsePacket.getPort(),request,response);}}private String process(String request) {return request;}public static void main(String[] args) throws IOException {UDPEchoServer udpEchoClient=new UDPEchoServer(9090);udpEchoClient.start();}
}
TCPEchoClient
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(String ip,int port) throws IOException {socket=new Socket(ip,port);}public void start() throws IOException {System.out.println("客户端启动!");try(OutputStream outputStream=socket.getOutputStream();InputStream inputStream=socket.getInputStream()){Scanner scanner=new Scanner(inputStream);Scanner scannerIn=new Scanner(System.in);PrintWriter printWriter=new PrintWriter(outputStream);while(true){//1.从控制台读取数据System.out.println("输入请求内容");String request= scannerIn.next();//2.把请求发送给服务器printWriter.println(request);printWriter.flush();//3.从服务器中读取响应while(!scanner.hasNext()){break;}String response=scanner.next();//4.打印响应结果System.out.println(response);}}catch (IOException e){e.printStackTrace();}finally {socket.close();}}public static void main(String[] args) throws IOException {TCPEchoClient client=new TCPEchoClient("127.0.0.1",9090);client.start();}}
TCPEchoServer
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("启动服务器");while(true){//多线程形式Socket clientSocket = serverSocket.accept();/* Thread thread=new Thread(()->{try {processConnection(clientSocket);} catch (IOException e) {e.printStackTrace();}});*/ExecutorService pool= Executors.newCachedThreadPool();pool.submit(new Runnable() {@Overridepublic void run() {try {processConnection(clientSocket);} catch (IOException e) {e.printStackTrace();}}});}}//针对一个链接提供处理逻辑private void processConnection(Socket clientSocket) throws IOException {//先打印客户端信息System.out.println("客户端上线!!!");//获取socket中持有的流对象try(InputStream inputStream=clientSocket.getInputStream();OutputStream outputStream=clientSocket.getOutputStream()){Scanner scanner=new Scanner(inputStream);PrintWriter printWriter=new PrintWriter(outputStream);//使用scanner包装一下inputStream吗,就可以方便的读取这里的请求数据了while(true){//1.读取请求并解析if(!scanner.hasNext()){//没有数据可读的时候返回true,连接断开返回falsebreak;//连接打开没有数据到来的时候阻塞}String request=scanner.next();//2.根据请求计算响应String response=process(request);//3.把响应写回给客户端printWriter.println(response);//将数据写回给客户端printWriter.flush();//将缓冲区的数据冲刷下//打印日志System.out.printf("[%s:%d] request=%s response=%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);}}catch (IOException e){e.printStackTrace();}finally {System.out.printf("【%s:%d】客户端下线!!!\n",clientSocket.getInetAddress(),clientSocket.getPort());clientSocket.close();}}private String process(String request) {//回显服务器return request;}public static void main(String[] args) throws IOException {TCPEchoServer server=new TCPEchoServer(9090);server.start();}
}