Java学习历程28——网络编程初步 [ 通信系统(1)]

发布时间:2026/7/26 6:47:40
Java学习历程28——网络编程初步 [ 通信系统(1)] 在日常生活中我们所使用的网页和APP都是在网络的条件下运行的由客户端向服务端发起请求再由服务端响应实现客户端想要进行的操作这就是网络编程的产物涉及到通信和计算机网络的相关知识。在本篇博客中我们将以一个本地的通信控制台消息程序初步了解网络编程的相关知识。目录一、计算机网络的基础知识①IP地址——网络中的门牌号②端口——程序中的门牌号③TCP vs UDP——网络协议二、通信小程序开发服务端①初始化ServerSocket并绑定端口②创建监听线程等待客户端连接③为每一个客户端创建独立的接收线程④消息格式的设计⭐⭐⭐⑤消息的接收与转发客户端①连接服务端②获取输入输出流③创建独立的接收线程④主线程处理用户的输入和发送三、补充知识①ServerSocket和Socket核心常用方法②InputStream 和 OutputStream核心常用方法InputStream inOutputStream outa. in.read()b. out.write()c. out.flush()③字节流传输四、完整代码服务端客户端五、效果展示一、计算机网络的基础知识在开始写代码之前得先搞清楚几个最基本的概念。这就好比你要寄快递得知道收件人的地址IP、具体的门牌号端口以及通过什么方式寄协议。①IP地址——网络中的门牌号IP地址用来标识互联网上的每一台设备就像我们的家庭住址一样。IPv4地址是32位的通常写成点分十进制的形式比如 192.168.1.100。我们平时最常遇到的就是两类IP公网IP在全球互联网上唯一的地址可以直接被访问私网IP只在局域网内使用比如家里的路由器会给每台设备分配一个 192.168.x.x 的地址还有一个特殊的IP叫127.0.0.1也就是localhost表示本机。当你用这个地址连接时数据不经过网卡直接在操作系统内部就绕回来了非常适合本地测试。②端口——程序中的门牌号IP地址找到了设备但一台电脑上运行着很多程序数据该交给谁呢这就用到了端口。端口号是一个16位的数字范围是0到65535。其中0-1023系统保留端口比如HTTP用80HTTPS用4431024-49151注册端口用户程序可以使用49152-65535动态端口通常由操作系统临时分配当我们写聊天程序时服务端需要绑定一个固定的端口比如54321客户端连接时就要指定这个端口。③TCP vs UDP——网络协议TCP就像打电话先拨号建立连接三次握手然后才能通话确保对方能收到每一句话。它可靠、有序但开销大一些适用于要求不能丢失数据的应用程序。UDP就像发短信不管对方在不在线直接发出去就行。它速度快但不保证一定能收到也不保证顺序适用于实时性要求较高的应用程序。特性TCPUDP连接面向连接三次握手无连接可靠性可靠确认重传不可靠尽力而为速度较慢较快适用场景文件传输、HTTP、聊天音视频、DNS、游戏二、通信小程序开发我们计划开发一个支持本地运行服务端同时连上多个客户端客户端可以发送信息至服务端并由服务端转发到各个客户端并且收发可以随时进行而不阻塞实现群聊的效果。服务端①初始化ServerSocket并绑定端口服务端第一步就是创建ServerSocket并绑定端口。new ServerSocket (xxx) 相当于告诉操作系统我要监听xxx端口所有发往这个端口的数据都交给我处理。②创建监听线程等待客户端连接服务端需要持续监听来自客户端的连接请求这时我们需要调用ServerSocket类中的accept方法。而由于这个方法是一个阻塞方法它会一直等待直到有客户端连接进来然后返回一个代表这个连接的Socket对象。正因为它是阻塞的我们才需要把它放在单独的线程中执行否则整个程序都会停在这里干不了别的事。这个监听线程会一直运行不断接收新的客户端连接。与此同时我们需要建一个Socket的动态数组用于存放所有客户端的Socket对象便于后续的使用在监听到后调用add方法即可。③为每一个客户端创建独立的接收线程当监听到一个新的客户端连接后我们需要为这个客户端创建一个独立的线程来处理它后续发送的所有消息。这样设计的核心原因是每个客户端的接收操作都是独立的一个客户端发了消息我们处理它的消息另一个客户端没发消息它的线程就在那里等着彼此互不干扰。具体实现是在监听线程的循环中每得到一个Socket对象就创建一个新的线程在线程内部通过socket.getInputStream ()获取输入流然后持续读取该客户端发来的数据。这里有一个关键点为什么不能把所有客户端的输入流放在一个线程里统一处理因为in.read()是阻塞的。如果线程A正在等待客户端1的数据客户端2此时发来了消息线程A根本不会知道——它被客户端1的读操作卡住了。每个客户端独立一个接收线程就解决了这个问题。④消息格式的设计⭐⭐⭐在真正开始收发消息之前我们需要先确定一个重要的细节消息在网络上怎么传输接收方怎么知道一条消息从哪里开始、到哪里结束我们的方案是使用长度前缀发送消息时先发送1个字节表示消息的长度再发送消息的内容本身。接收方先读取1个字节获得长度再根据这个长度读取对应数量的字节组装成完整的消息。这是网络信息传输的经典做法这样做的好处是简单直接而且能有效解决TCP的粘包问题——因为TCP是流式传输没有消息边界如果没有长度前缀接收方无法判断一条消息的结束位置和下一条消息的开始位置。⑤消息的接收与转发每个客户端的接收线程通过 InputStream 持续读取消息。读取时按照约定的格式先读1个字节获得消息长度然后循环读取直到读满指定长度的字节最后将这些字节转换成字符串得到完整的消息内容。收到消息后服务端需要将它转发给所有在线的客户端实现群聊的效果。具体做法是遍历存储所有客户端Socket的动态数组对数组中的每一个Socket除了发送者自己获取其输出流将消息以相同的格式长度前缀内容发送出去。客户端①连接服务端客户端第一步就是通过 new Socket(127.0.0.1 , 端口号) 连接到服务端。这里的127.0.0.1是本地回环地址表示连接本机上的服务端xxx是服务端绑定的端口号。连接成功后会得到一个Socket对象通过这个对象就可以和服务端进行通信了。②获取输入输出流连接建立后通过Socket对象获取输入流和输出流。Socket.getInputStream() 用于接收服务端发来的消息Socket.getOutputStream() 用于向服务端发送消息。这两个流是TCP连接的数据通道所有收发操作都通过它们完成。③创建独立的接收线程和服务端类似客户端也需要一个独立的线程来持续接收服务端发来的消息。为什么不能把接收放在主线程因为in.read()是阻塞的如果放在主线程里程序会卡在等待服务端消息的位置用户就无法输入新的消息了。在接收线程中通过in.read()先读取1个字节获得消息长度然后根据长度循环读取对应数量的字节最后将字节数组转换成字符串打印出来则可以将服务端转发的消息显示在客户端的控制台上。④主线程处理用户的输入和发送主线程负责读取用户在控制台输入的内容并发送给服务端。通过Scanner的nextLine()方法获取用户输入的一行字符串然后按照约定的消息格式发送先获取消息内容的字节数组将字节数组的长度1个字节写入输出流再写入字节数组本身最后调用 flush()确保数据立即发送出去。这样收发分离的设计使得用户可以随时输入消息发送同时也能随时接收并显示服务端转发来的消息实现了真正的双向通信。三、补充知识①ServerSocket和Socket在整个网络编程中ServerSocket和Socket是两个最核心的类。ServerSocket负责监听和接受连接请求可以把它理解成总机接线员它只做一件事——等着电话打进来。它的accept方法会一直阻塞直到有客户端连接进来然后返回一个Socket对象。这个Socket就是服务端和客户端之间通信的通道。Socket类代表一个TCP连接的两端。在服务端它是accept方法的返回值在客户端它是new Socket(ip, port)构造出来的。无论哪一端一旦拿到了Socket对象就可以通过getInputStream和getOutputStream获取输入输出流进行数据的收发。核心常用方法new ServerSocket(int port)构造方法绑定到指定端口Socket.accept()阻塞等待客户端连接返回连接上的Socketnew Socket(String host, int port)客户端构造方法连接到指定服务端InputStream getInputStream()获取输入流用于读取对方发来的数据OutputStream getOutputStream()获取输出流用于向对方发送数据②InputStream 和 OutputStreamInputStream和OutputStream是所有字节输入输出流的父类。在网络编程中它们负责在Socket之间传输原始字节数据。输入流指的是该socket接受其他socket消息的通道而输出流是向其他socket发送信息的通道。核心常用方法InputStream inOutputStream outa. in.read()该方法用于将输入流中的数据读取出来其返回值是int代表该字节对应的ASCII。有三种常用的重载方法一种是无形参的用于读取输入流中的一个字节另一种是以字节数组为形参的用于将输入流中的所有字节存入该字节数组中还有一种是int read(byte[] b, int off, int len)表示读取输入流中len长度的字节从数组b的off位置开始存放字节数据。b. out.write()该方法用于将数据写入输出流中无返回值其重载方法和read类似可以是一个字节也可以是一个字节数组也可以是一个字节数组某个位置开始的某长度。c. out.flush()该方法用于将缓冲区的数据强制刷新到底层输出流确保数据立即发送。在网络编程中数据不会在调用write后立即发送而是积累在缓冲区中等缓冲区满了或者手动调用flush才会发送。所以我们发送完消息后一定要调用flush。如果不调用flush数据可能会在缓冲区里积压迟迟到不了对方手中。③字节流传输在本项目中我们采用字节的方式进行消息的传输。网络传输的本质是字节的传输所谓的字符流不过是在字节流上加了编码的转换所以在传输时使用字节可以避免编码混乱导致乱码的问题并且字节流可以传输二进制数据如文件、数据等。在我们的代码中我们用字节数组byte[ ] msg先接收传输过来的信息再使用new String(msg)将字节转换成字符串打印完全控制了编码的过程不会出现乱码的问题。四、完整代码服务端import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; public class Server { ServerSocket serverSocket null; // 服务端初始化 public void init() { try { serverSocket new ServerSocket(54321); System.out.println(Server Started); } catch (IOException e) { throw new RuntimeException(e); } } // 监听连接 public Socket listen() { try { return serverSocket.accept(); } catch (IOException e) { throw new RuntimeException(e); } } // 接收信息 public String receive(InputStream in) { String msgString; try { int len in.read(); byte[] msg new byte[len]; // 直接读取后面的所有字节存进字节数组 in.read(msg); msgString new String(msg); System.out.println(msgString); } catch (IOException e) { throw new RuntimeException(e); } return msgString; } // 服务端接受到信息并转发至各个客户端一个客户端用一个线程 public void transpond(Socket socket, ArrayListSocket socketList) { new Thread(() - { while (true) { try { InputStream in socket.getInputStream(); String msg receive(in); byte[] msgBytes msg.getBytes(); for (Socket s : socketList) { if(!s.equals(socket)) { OutputStream out s.getOutputStream(); out.write(msgBytes.length); out.write(msgBytes); out.flush(); } } } catch (IOException e) { throw new RuntimeException(e); } } }).start(); } public static void main(String[] args) { Server server new Server(); ArrayListSocket SocketList new ArrayList(); server.init(); // 专门监听接入连接的线程 new Thread(() - { while (true) { Socket socket server.listen(); SocketList.add(socket); // 接受信息同时转发 server.transpond(socket, SocketList); } }).start(); } }客户端import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.util.Scanner; public class Client { // 接受消息 public void receive(InputStream in) { // 单独开一个线程接受来自服务端的消息 new Thread(() - { while (true) { int receiveLen; try { receiveLen in.read(); for (int i 0; i receiveLen; i) { System.out.print((char) in.read()); } System.out.println(); } catch (IOException e) { throw new RuntimeException(e); } } }).start(); } public static void main(String[] args) { Client client new Client(); try { // 套接字 Socket sc new Socket(127.0.0.1, 54321); // 输入输出流 InputStream in sc.getInputStream(); OutputStream out sc.getOutputStream(); Scanner scanner new Scanner(System.in); // 接收信息 client.receive(in); // 发送信息 while (true) { String msg scanner.nextLine(); int len msg.length(); out.write(len); byte[] b msg.getBytes(); out.write(b); // 点击发送 out.flush(); } } catch (IOException e) { throw new RuntimeException(e); } } }五、效果展示注意我们需要先启动服务端在客户端的运行设置中设置为“允许运行多个实例”开启多个客户端的实例连上服务端然后进行测试。这样我们就初步实现了群聊的效果后续的开发方向是认证系统和界面化等高级功能。