当前位置: 首页 > news >正文

硅基计划6.0 JavaEE 肆 网络网络编程

1758766225692


文章目录

  • 一、基本内容
  • 二、网络编程——代码通信
    • 1. TCP&UDP区别
    • 2. UDP网络编程
      • 1. 核心API介绍
      • 2. 示例讲解
    • 3. TCP网路编程
      • 1. 核心API讲解
      • 2. 示例讲解


一、基本内容

网路协议的分层原理,很多学校的教材都是讲的OSI七层网络模型,但是我们按照更准确的情况来说,应该是TCP/IP五层网络模型

  1. 物理层:就是硬件设备,约定了硬件协议,比如网线结构、光纤结构等等
  2. 传输层(操作系统内核):通信的起点和终点之间的通信规则,不关心具体的通信过程
  3. 网路层(操作系统内核):明确通信的起点和终点之间的路线规划
  4. 数据应用层:负责两个相邻节点的具体传输过程
  5. 应用层:我们写的代码和应用层绑定,决定了应用程序具体去如何使用

比如我们发送一个消息你好,作为发送方

  1. 将字符串进行串行化成一个数据包,调用传输层接口
  2. 在传输层假设使用UDP协议,进行再次封装调用网络层接口
  3. 在网络层假设使用IP协议,进行再次封装调用数据链路层接口
  4. 在数据链路层假设使用以太网协议,进行再次封装给物理层,通过网卡转换成电磁波传输
    数据中间进行不断的中转、传输…
    而作为接收方,把上述四步操作逆向,就可以拿到对方发送的内容

二、网络编程——代码通信

套接字,英文名Socket,它是对网路编程的所有函数的统称,我们两只不过网络协议TCPUDP提供了两套不一样的API供去调用

1. TCP&UDP区别

  1. TCP:有链接、可靠传输、面向字节流、全双工
  2. UDP:无连接、不可靠传输、面向数据报、全双工
  • 连接:通常指的是网络连。对于TCP,通信的双方都保存了对方的核心信息,连接断开,对应的关键信息就销毁;而对于UDP,则是采用直接发送的方式
  • 可靠传输/不可靠传输:每一个交换机或者是路由器的单位时间内能够传输的数据量是有限的,超过了就会产生丢包,而可靠传输为了对抗丢包,因此会把信息尽可能的传输给对方,但是代价就是效率的下降;对于不可靠传输,则不会考虑这些
  • 面向字节流&面向数据报:字节流读取方式灵活,和我们前面写的文件IO类似;而对于数据报,我们每次读取都是以一个UDP数据报为单位进行
  • 全双工&半双工:全双工指的是通信双方可以相互通信;而半双工只能单方面

2. UDP网络编程

1. 核心API介绍

  • DatagramSocket:本质上是一个Socket对象,相当于遥控器,后续我们对Socket对象对象的操控就通过这个进行
  • DatagramPacket:是UDP传输的基本单位,即数据报

2. 示例讲解

需求:使用UDP回显服务器接收客户端发来的请求,再把这个请求原封不动的返回给客户端

我们来创建两个类,一个是客户端UdpEchoClient,另一个是服务器端UdpEchoServer

public class UdpEchoServer {//创建数据报private DatagramSocket socket;//构造方法制定端口号public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}
}

我们再来讲讲什么是端口号
说白了就是区分同一台主机上不同的应用程序,我们需手动配置服务器的端口号,以便让客户端知道把请求发给哪个应用程序
取值一般是0~65535,不要选择0~1023,这些是一些系统底层的程序,不要冲突


我们服务器要长期接受来自客户端的请求,再把我么服务器的响应发送给客户端
因此我们服务器的基本设置按照三个步骤
读取请求并解析-->根据请求计算响应-->响应返回给客户端
如果我们没有在客户端上读取到请求,我们就等待,即阻塞状态,因此我们来撰写start()启动服务器方法

public class UdpEchoServer {//创建数据报private DatagramSocket socket;//构造方法制定端口号public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}//我们继续启动服务器public void start() throws IOException {//保证服务器长期运行,因此使用死循环while(true){//我们手动分配一个缓冲区的内存空间数组,并且指定缓冲区的长度//本质上还是一个输出型参数,相当于白纸,等待数据来填充DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);socket.receive(requestPacket);//我们是面向数据报的,因此我们使用字符串进行传输//获取之前创建的字节数组,以及对应的偏移量和长度String request = new String(requestPacket.getData(),0,requestPacket.getLength());//我们再把上述请求发给回显服务器,再接收结果String response = process(request);//我们不可以使用size代替length,因为size记录的是字符数,而我们length求的是字节数,最后再明确把这个响应发送给谁DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());socket.send(responsePacket);//再打印发送日志System.out.printf("[%s:%d] request:%s,response:%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(9009);server.start();}
}

而对于客户端的创建呢,不需要我们自己去指定端口号,服务器它会自己去分配,避免和其他应用程序产生冲突

public class UdpEchoClient {private DatagramSocket socket;private String serverIp;//服务器IP地址private int serverPort;//服务器端口号//我们在构建客户端的时候,把服务器IP和端口号只指定下public UdpEchoClient(String serverIp,int serverPort) throws SocketException {socket = new DatagramSocket();this.serverIp = serverIp;this.serverPort = serverPort;}public void start() throws IOException {System.out.println("客户端启动!!");while(true) {//客户端用户开始输入请求Scanner sc = new Scanner(System.in);String request = sc.next();//我们以字节为单位生成数据报,同时我们也可以来判断客户端是否要进行退出操作if (request.equals("0")) {break;}//同时也要明确服务器的IP以及端口号,我们上面已经指定好了DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(serverIp), this.serverPort);socket.send(requestPacket);//发送DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);//接收返回的结果,并将结果放入输出线参数中socket.receive(responsePacket);String response = new String(responsePacket.getData(),0,responsePacket.getLength());//显示结果System.out.println("服务器返回结果是:"+response);}}public static void main(String[] args) throws IOException {UdpEchoClient client = new UdpEchoClient("127.0.0.1",9009);client.start();}
}

最终结果如下图所示

image-20251109155926653

我们在此基础上,实现一个基础的根据字典单词翻译的服务器,我们直接使用extends集成现有的服务器类,再拓展对应功能
因为我们是继承,因此客户端会自动优先调用子类,因此也就可以直接调用我们的翻译服务器

public class UdpTransServer extends UdpEchoServer{//翻译词典private HashMap<String,String> hash = new HashMap<>();public UdpTransServer(int port) throws IOException{super(port);//加入一些词典单词hash.put("hello","你好");hash.put("world","世界");hash.put("apple","苹果");hash.put("tree","树");}@Overridepublic String process(String request){return hash.getOrDefault(request,"未找到该单词");}public static void main(String[] args) throws IOException {UdpTransServer server = new UdpTransServer(9009);server.start();}
}

image-20251109161135672

3. TCP网路编程

1. 核心API讲解

  • ServerSocket:给服务器使用,负责处理连接到服务器的客户端
  • Socket:给服务器和客户端使用,针对每一个客户端提供具体服务

2. 示例讲解

我们对于服务器的构建,总共还是那四步读取请求并解析-->根据请求计算响应-->响应返回给客户端

public class TcpEchoServer {private ServerSocket serverSocket;public TcpEchoServer(int port) throws IOException{serverSocket = new ServerSocket(port);}public void start() throws IOException {while(true){//接受客户端请求并且连接,具体内部已经由操作系统实现//我们接下来就要把这个操作系统连接放到应用程序中,如果客户端没有连接此时会陷入阻塞状态Socket socket = serverSocket.accept();//处理连接的过程processConnection(socket);}}private void processConnection(Socket socket) {//我们需要生成一个长连接,处理多个请求(短连接只能处理一个请求)System.out.printf("[%s:%d 客户端上线\n",socket.getInetAddress(),socket.getPort());//由于我们面向的是字节流,因此我们要使用try-with处理字节流对象,处理很多个请求try(InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()){//使用Scanner解析Scanner sc = new Scanner(inputStream);//使用Writer子类格式化写入到输出流中PrintWriter printWriter = new PrintWriter(outputStream);//接下来我们针对每一个请求进行处理while (true){if(!sc.hasNext()){//如果没有输入流信息,则说明客户端关闭了连接break;}String request = sc.next();String response = process(request);//我们再把结果写回到客户端printWriter.write(request);System.out.printf("[%s:%d] request:%s,response:%s\n",socket.getInetAddress().toString(),socket.getPort(),socket,socket);}}catch (IOException e){e.printStackTrace();}}public String process(String request) {return "哇酷哇酷"+request;}public static void main(String[] args) throws IOException {TcpEchoServer server = new TcpEchoServer(9010);server.start();}
}

对于客户端,我们也是差不多的原理,只是我们需要在构造方法中自己制定服务器IP地址和端口号

public class TcpEchoClient {private Socket socket;public TcpEchoClient(String serverIp, int serverPort) throws IOException {socket = new Socket(serverIp, serverPort);}public void start() {try (InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()) {Scanner sc = new Scanner(System.in);//针对服务器的响应,我们也要使用Scanner进行包装Scanner scOut = new Scanner(inputStream);PrintWriter printWriter = new PrintWriter(outputStream);while (true) {String request = sc.next();//格式化包装发给服务器printWriter.println(request);String response = scOut.next();System.out.println(response);}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) throws IOException{TcpEchoClient client = new TcpEchoClient("127.0.0.1",9010);client.start();}
}

image-20251109165824219


但是当我们运行代码后,却发现我们客户端输入内容后服务器未响应,并且服务器端也没有接收到客户端的信息

针对这个问题,其实就是我们字节流中的内容不会直接到内存中,而是在一个缓冲区,只有当缓冲区满了才写入内存
并且在我们的Writer之类的PrintWriter也内置了字节流,因此我们如何让程序知道我们不写入缓冲区直接写入内存呢,诶,我们使用wirter.flush(),因此我们的服务器端的processConnection()方法改成

public class TcpEchoServer {private void processConnection(Socket socket) {//我们需要生成一个长连接,处理多个请求(短连接只能处理一个请求)System.out.printf("[%s:%d 客户端上线\n",socket.getInetAddress(),socket.getPort());//由于我们面向的是字节流,因此我们要使用try-with处理字节流对象,处理很多个请求try(InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()){//使用Scanner解析Scanner sc = new Scanner(inputStream);//使用Writer子类格式化写入到输出流中PrintWriter printWriter = new PrintWriter(outputStream);//接下来我们针对每一个请求进行处理while (true){if(!sc.hasNext()){//如果没有输入流信息,则说明客户端关闭了连接break;}String request = sc.next();String response = process(request);//我们再把结果写回到客户端printWriter.write(response);//改动printWriter.flush();//直接写入内存System.out.printf("[%s:%d] request:%s,response:%s\n",socket.getInetAddress().toString(),socket.getPort(),request,response);}}catch (IOException e){e.printStackTrace();}}
}

同时我们客户端的start()方法也改成

public class TcpEchoClient {public void start() {try (InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()) {Scanner sc = new Scanner(System.in);//针对服务器的响应,我们也要使用Scanner进行包装Scanner scOut = new Scanner(inputStream);PrintWriter printWriter = new PrintWriter(outputStream);while (true) {String request = sc.next();//格式化包装发给服务器printWriter.println(request);//改动printWriter.flush();//直接写入内存String response = scOut.next();System.out.println(response);}} catch (IOException e) {e.printStackTrace();}}
}

但是我们目前代码还是存在文件资源泄露风险,是因为在

public void start() throws IOException {while(true){// 这一条语句Socket socket = serverSocket.accept();processConnection(socket);}}

我们每一次accept就会在内部new一个socket,就会导致文件描述附表被占用
如果我们不及时清理,创建的socket也就会越来越多,因此我们要在processConnection中添加finally代码块,以便关闭资源

private void processConnection(Socket socket) throws IOException {//我们需要生成一个长连接,处理多个请求(短连接只能处理一个请求)System.out.printf("[%s:%d 客户端上线\n",socket.getInetAddress(),socket.getPort());//由于我们面向的是字节流,因此我们要使用try-with处理字节流对象,处理很多个请求try(InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()){//使用Scanner解析Scanner sc = new Scanner(inputStream);//使用Writer子类格式化写入到输出流中PrintWriter printWriter = new PrintWriter(outputStream);//接下来我们针对每一个请求进行处理while (true){if(!sc.hasNext()){//如果没有输入流信息,则说明客户端关闭了连接break;}String request = sc.next();String response = process(request);printWriter.flush();//直接写入内存//我们再把结果写回到客户端printWriter.println(response);System.out.printf("[%s:%d] request:%s,response:%s\n",socket.getInetAddress().toString(),socket.getPort(),request,response);}}catch (IOException e){e.printStackTrace();}finally {socket.close();}}

但是目前还是有问题,比如我们使用多个客户端的时候,其中一个客户端在调用服务器,其他客户端会无响应

image-20251109174046399

这是因为我们在创建多个客户端之后,我们的while循环中的代码serverSocket.accept()只能为一个客户端服务,进入了precessConnection()方法后就无法再出来,导致其他客户端无法调用accept()方法
因此我们两个方法要分开不在一个循环之内,这是我们想到了使用多线程,因此我们客户端的start()方法就改成

public void start() throws IOException {while(true){//接受客户端请求并且连接,具体内部已经由操作系统实现//我们接下来就要把这个操作系统连接放到应用程序中,如果客户端没有连接此时会陷入阻塞状态Socket socket = serverSocket.accept();Thread t = new Thread(()->{//处理连接的过程try {processConnection(socket);} catch (IOException e) {throw new RuntimeException(e);}});t.start();}}

image-20251109174618744


但是,万一我们客户端太多,创建了太多线程怎么办??
因此我们引入线程池概念

public void start() throws IOException {while(true){ExecutorService executors = Executors.newCachedThreadPool();//接受客户端请求并且连接,具体内部已经由操作系统实现//我们接下来就要把这个操作系统连接放到应用程序中,如果客户端没有连接此时会陷入阻塞状态Socket socket = serverSocket.accept();//我们把任务提交到线程池中,重复利用已有的线程,避免线程频繁的创建和销毁executors.submit(()->{//处理连接的过程try {processConnection(socket);} catch (IOException e) {throw new RuntimeException(e);}});}}

但是如果我们客户端的每一个线程占用时间很长呢,并且已经占用了的一些线程又不执行对应的逻辑,即僵尸线程,就会导致这个文件操作符表还是会溢出,怎么办呢

我们可以去使用线程池的IO复用,但是我们在Java中很少使用官方提供的NIO,因为其太过复杂
这个IO复用本质上是一个线程同时处理多个Socket,因为一个Socket并非一直活跃,即并不是一直要传输数据,我们通过通知机制来表示如果一个客户端有需求需要处理,我们再让这个客户端对应的Socket通知线程来处理

举个例子,你去买炒面,水饺,炒饭,你肯定是买了炒面后直接去买炒饭再买水饺,三个一起等,哪个好了就去拿


不知你是否发现,我们在代码中都是使用**\n**表示一个个请求,我们在读取请求的时候,都是next()方法
它读取到空白符(诸如回车、空格等等)都会停止读取,假设采用print就无法正差读取


我们还是使用TCP来实现刚刚的翻译服务器把

public class TcpEchoClient {private Socket socket;public TcpEchoClient(String serverIp, int serverPort) throws IOException {socket = new Socket(serverIp, serverPort);}public void start() {try (InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()) {Scanner sc = new Scanner(System.in);//针对服务器的响应,我们也要使用Scanner进行包装Scanner scOut = new Scanner(inputStream);PrintWriter printWriter = new PrintWriter(outputStream);while (true) {String request = sc.next();//格式化包装发给服务器printWriter.println(request);printWriter.flush();//直接写入内存if(!scOut.hasNext()){break;}String response = scOut.next();System.out.println("服务器返回的结果是"+response);}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) throws IOException{TcpEchoClient client = new TcpEchoClient("127.0.0.1",9010);client.start();}
}

image-20251109180228951


文章可能有错误欢迎指出

Git码云仓库链接

END♪٩(´ω`)و♪`
http://www.dtcms.com/a/590625.html

相关文章:

  • 南宁网站建设建站系统wordpress仿微博发文插件
  • 邯郸推广网站建设哪个好济南网站免费制作
  • 在线制作图片热区搜索引擎优化的主题
  • 字符串贪心:字典序 回文 括号
  • 实现AI和BI整合的初步思路和探索
  • 徐州网站关键词各类手机网站建设
  • 做音乐网站赚钱吗中信云做网站
  • 兰州网站建设平台分析指数函数图像
  • 从零掌握U-Net数据集训练:原理到实战的完整指南
  • 石家庄行业网站建设阿里巴巴官网入口
  • 来广营做网站公司游戏平台搭建
  • 【数值分析】12-非线性方程的求根方法-习题(1-8)
  • 焦作网站建设设计公司上海建设工程招投标在什么网站
  • 惠州建设网站开发汕头达濠
  • 位置编码演进史:SIN → ALiBi → RoPE → PI → NTK → YARN
  • 网站建设是必须的吗苏州工业园区两学一做教育网站
  • 鹤峰网站制作win7下使用wordpress
  • ThreadLocal中key为什么是弱引用,value为什么是强引用
  • 天津刘金鹏做网站网站手机版管理链接
  • 天津建设协会网站首页什么叫一级域名二级域名
  • YesPlayMusic v0.4.10 | 一款网易云第三方开源音乐播放器,同时支持切换其他酷我、QQ等音源
  • 手机网站建设浩森宇特网络营销的优势和劣势
  • 哪里学网站建设推广用wordpress建网站
  • 企业网站开发成都网页开发者选项在哪里
  • AI空间低配版?没有新品也能体验,极空间部署Foxel网盘
  • 如何建立内部网站怎么做hello官方网站
  • 常见的营销手段深圳网络营销优化
  • 重庆市公共资源交易中心专业做seo的网站
  • 企业门户网站源码下载做网站协调
  • 网站图片轮播怎么弄网站前端切图做多个页面