一文必搞懂-深入理解WebSocket并搭建简易聊天室

WebSocket

  • WebSocket
    • WebSocket特点
    • 代码基本框架
      • 客户端
      • 服务端
      • 代码示例
      • 发送与接收
        • 客户端实现
      • 服务端实现
      • 长连接
    • 在线聊天室
      • 设计思路
        • 客户端
        • 服务端代码
        • 存在的问题
      • 用户身份
        • 解决方法
      • 用户姓名的显示代码
      • 并发优化
        • 解决方法

WebSocket

  • WebSocket,是一个基于TCP的通讯协议,适用于网站应用。
  • HTTP:短连接的TCP协议
  • WebSocket: 长连接的TCP协议
  • 常见的应用场景:基于Web的聊天室、在线客服

WebSocket特点

  1. 单一TCP长连接,采用全双工通信模式(HTTP是半双工,只能一请求,一答案,而不能同时进行)
  2. 对代理、防火墙透明
  3. 无头部信息、消息更精简
  4. 通过ping/pong 来保活
  5. 服务器可以主动推送消息给客户端,不在需要客户轮询

代码基本框架

WebSocket的代码框架:

一文必搞懂-深入理解WebSocket并搭建简易聊天室_第1张图片

  • 客户端:用JS创建一个 WebSocket
  • 服务端:用 @ServerEndPoint 创建一个服务

客户端

  1. 创建 WebSocket对象
  2. 指定服务地址
    ws://127.0.0.1:8080/demo/websocket/chat
  3. 指定事件处理的回调方法
    onopen(), onclose(), onerror(), onmessage()
  4. 发送消息 sock.send(msg)

服务端

  1. @ServerEndpoint("/websocket/chat")
    指定服务地址
  2. @OnOpen @OnClose @OnError @OnMessage
    分别指定4个事件的回调;
    EndPoint: 一个服务;
    Session: 代表了一路客户端连接;

注意:
服务地址
ws://127.0.0.1:8080/demo/websocket/chat,ws:// 表示协议为websocket,与 http:// 区分javax.websocket.Session代表了一个客户端连接

代码示例

客户端:chat.html


<html>
	<head>
		<meta charset="utf-8">
		<title>title>
		
		<script src="js/jquery.min.js" >script>
		<script src="js/afquery.js" >script>
		<link rel="stylesheet" href="css/common.css" />
		
	head>
	<body>
		<div>
			
		div>
	body>

	<script>

		// 打开WebSocket
		// ws://127.0.0.1:8080/demo/websocket/chat
		var sock = new WebSocket("ws://" + location.host + "/demo/websocket/chat");
	
		// 指定回调方法
		//连接完成触发的回调方法
		sock.onopen = function() {
			console.log('Client Socket Open');
		};
		//连接关闭触发的回调方法
		sock.onclose = function(event) {
			console.log('Client Socket Closed');
		};
		//连接通讯发生异常触发的回调方法
		sock.onerror = function(event) {
			console.log('Client Socket Error');
		};
		//通讯过程中受到信息触发的回调方法
		sock.onmessage = function(event) {
			console.log('** Client Receive:' + event.data);
		};
	script>
html>

服务端:

package my;

import java.io.IOException;

import javax.websocket.EndpointConfig;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/websocket/chat")
public class ChatEndpoint
{
	Session session; // 一个WebSocket连接,不是 HttpSession
	
	public ChatEndpoint()
	{
		System.out.println("** 创建一个Endpoint...");
	}
	//连接完成触发的回调方法
	@OnOpen
	public void onOpen(Session session, EndpointConfig config)
	{		
		this.session = session;
		System.out.println("** 新的用户连接: ");
	}
	//通讯连接关闭触发的回调方法
	@OnClose
	public void onClose()
	{	
		System.out.println("** 用户连接已关闭");
	}
	//通讯发生异常触发的回调方法
	@OnError
	public void onError(Session session, Throwable error)
	{
		System.out.println("** 用户连接出错");
		error.printStackTrace();
	}
	//通讯过程中受到信息触发的回调方法
	@OnMessage
	public void onMessage(String message, Session session)
	{
		System.out.println("用户消息:" + message);
		
		try
		{
			session.getBasicRemote().sendText("OK, welcome");
		} catch (IOException e)
		{
			e.printStackTrace();
		}
	}


}

不需要记住这些代码,但是要记住发生的四个回调事件。

发送与接收

  • 接收:由onmessage自动处理
  • 发送: 等下看;

客户端实现

  • sock.onmessage( event ):自动接收服务器发来的消息,event.data即为消息内容
  • sock.send ( message ):调用send()方法即可以向服务器发送消息


<html>
	<head>
		<meta charset="utf-8">
		<title>title>
		
		<script src="js/jquery.min.js" >script>
		<script src="js/afquery.js" >script>
		<link rel="stylesheet" href="css/common.css" />
		
		<style>
			.container{
				margin: 30px auto auto auto;
				width: 800px;
			}
			.line{
				margin: 10px 0;
			}
			.message{
				width: calc(100% - 80px);
				padding: 6px;
			}
			.chatroot{
				width: 100%;
				height: 300px;
				font-size: 15px;
				line-height: 130%;
			}
		style>
	head>
	<body>
		<div class='container'>
			<div class='line'>
				<input type='text' class='message'>
				<button onclick='my.doSend()'> 发送 button>
			div>
			<div class='line'>
				<textarea class='chatroot'>textarea>
			div>
		div>
	body>

	<script>

		// 打开WebSocket
		// ws://127.0.0.1:8080/demo/websocket/chat
		var sock = new WebSocket("ws://" + location.host + "/demo/websocket/chat");
	
		// 指定回调方法
		sock.onopen = function() {
			console.log('Client Socket Open');
		};
		sock.onclose = function(event) {
			console.log('Client Socket Closed');
		};
		sock.onerror = function(event) {
			console.log('Client Socket Error');
		};
		sock.onmessage = function(event) {
			console.log('** Client Receive:' + event.data);
			my.showMessage('<< ' + event.data);
		};

		var my = {};
		
		// 发送
		my.doSend = function(){
			var message = $('.message').val();
			my.showMessage('>> ' + message );
			
			// 发送消息
			sock.send( message );		
		}
		
		// 显示消息
		my.showMessage = function(message){
			$('.chatroot').append(message + '\n');
		}
	script>
html>

服务端实现

  • @OnMessage:自动接收客户端发来的消息
  • session.getBasicRemote().sendText():向客户端发送消息
  • 在ChatEndpoint.onOpen()里,记录当前Session
  • 在服务端,由注解来指定回调。回调方法的名称是可以任意的,参数形式是确定的
  • 服务端的EndPoint是多例:每一个客户端连接上来,服务器就创建一个ChatEndPoint对象与用户连接关联
  • 注意:因为EndPoint是多例的,在Spring IOC注入过程,EndPoint类用@Component注解修饰注入容器,由于IOC只会在应用启动过程中进行属性填充,如果我们再EndPoint里注入了一些属性(RestTemplate等),那么第一个连接属性才有填充,第二及以后的属性会报空指针异常

```java
package my;

import java.io.IOException;

import javax.websocket.EndpointConfig;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/websocket/chat")
public class ChatEndpoint
{
	Session session; // 一个WebSocket连接,不是 HttpSession
	
	public ChatEndpoint()
	{
		System.out.println("** 创建一个Endpoint...");
	}
	
	@OnOpen
	public void onOpen(Session session, EndpointConfig config)
	{		
		this.session = session;
		System.out.println("** 新的用户连接: ");
	}

	@OnClose
	public void onClose()
	{	
		System.out.println("** 用户连接已关闭");
	}

	@OnError
	public void onError(Session session, Throwable error)
	{
		System.out.println("** 用户连接出错");
		error.printStackTrace();
	}
	
	@OnMessage
	public void onMessage(String message, Session session)
	{
		System.out.println("用户消息:" + message);
		
		try
		{
			session.getBasicRemote().sendText("OK, welcome");
		} catch (IOException e)
		{
			e.printStackTrace();
		}
	}


}

长连接

  • WebSocket是长连接模式:一次连接,多次消息交互
  • WebSocket协议与 HTTP 协议是不同的
  • 首次交互相似
  • 后续的交互完全不同

在线聊天室

  • 在线聊天室:使用WebSocket,实现多人在线聊天
  • 每个人都可以发送消息,显示到聊天室里

设计思路

思路如下:

  1. 点‘发送’后,将消息发送到服务器
  2. 服务器把此消息转发给每一个用户
    这意味着,服务器端要记下每一个用户client1, client2, …, clientN.

添加一个管理器 ChatRoom ,由它维护所有的客户端列表

  • addClient( ) : 新客户连接时,添加一条记录
  • removeClient() : 客户断开时,移除一条记录
  • sendAll() : 给所有客户端发送一条消息

客户端

chat.html


<html>
	<head>
		<meta charset="utf-8">
		<title>title>
		
		<script src="js/jquery.min.js" >script>
		<script src="js/afquery.js" >script>
		<link rel="stylesheet" href="css/common.css" />
		
		<style>
			.container{
				margin: 30px auto auto auto;
				width: 800px;
			}
			.line{
				margin: 10px 0;
			}
			.message{
				width: calc(100% - 80px);
				padding: 6px;
			}
			.chatroot{
				width: 100%;
				height: 300px;
				font-size: 15px;
				line-height: 130%;
			}
		style>
	head>
	<body>
		<div class='container'>
			<div class='line'>
				<input type='text' class='message'>
				<button onclick='my.doSend()'> 发送 button>
			div>
			<div class='line'>
				<textarea class='chatroot'>textarea>
			div>
		div>
	body>

	<script>

		// 打开WebSocket
		// ws://127.0.0.1:8080/demo/websocket/chat
		var sock = new WebSocket("ws://" + location.host + "/demo/websocket/chat");
	
		// 指定回调方法
		sock.onopen = function() {
			console.log('Client Socket Open');
		};
		sock.onclose = function(event) {
			console.log('Client Socket Closed');
		};
		sock.onerror = function(event) {
			console.log('Client Socket Error');
		};
		sock.onmessage = function(event) {
			console.log('** Client Receive:' + event.data);
			my.showMessage(': ' + event.data);
		};

		var my = {};
		
		// 发送
		my.doSend = function(){
			var message = $('.message').val();
			// my.showMessage('>> ' + message );
			$('.message').val(''); // 清空输入
			
			// 发送消息
			sock.send( message );		
		}
		
		// 显示消息
		my.showMessage = function(message){
			$('.chatroot').append(message + '\n');
		}
	script>
html>

服务端代码

聊天室ChatRoom.java

package my;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class ChatRoom
{
	// 创建全局实例  ChatRoom.i 
	public static ChatRoom i = new ChatRoom();
	
	// 客户端的列表
	private List<ChatEndpoint> clientList = new LinkedList<>();
		
	// 当有客户端连接时,将此客户端添加到 clientList
	public void addClient(ChatEndpoint client)
	{
		synchronized (clientList)
		{
			clientList.add(client);
		}
	}
	
	// 当有客户端断开时,将此客户端从 clientList 移除
	public void removeClient(ChatEndpoint client)
	{
		synchronized (clientList)
		{
			clientList.remove(client);
		}
	}
	
	// 发给所有人
	public void sendAll(String message)
	{		
		// 1 并发问题
		// 2 效率问题
		synchronized (clientList)
		{
			Iterator iter = clientList.iterator();
			while(iter.hasNext())
			{
				ChatEndpoint client = (ChatEndpoint)iter.next();
				client.sendMessage( message );
			}
		}		
	}
}

长连接类ChatEndpoint.java

package my;

import java.io.IOException;

import javax.websocket.EndpointConfig;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/websocket/chat")
public class ChatEndpoint
{
	Session session; // 一个WebSocket连接,不是 HttpSession
	
	public ChatEndpoint()
	{
		System.out.println("** 创建一个Endpoint...");
	}
	
	@OnOpen
	public void onOpen(Session session, EndpointConfig config)
	{		
		this.session = session;
		ChatRoom.i.addClient(this);
		System.out.println("** 新的用户连接: ");
	}

	@OnClose
	public void onClose()
	{	
		ChatRoom.i.removeClient(this);
		System.out.println("** 用户连接已关闭");
	}

	@OnError
	public void onError(Session session, Throwable error)
	{
		ChatRoom.i.removeClient(this);
		System.out.println("** 用户连接出错");
		error.printStackTrace();
	}
	
	@OnMessage
	public void onMessage(String message, Session session)
	{
		System.out.println("用户消息:" + message);
		
		ChatRoom.i.sendAll(message);
	}

	public void sendMessage(String message)
	{
		try
		{
			session.getBasicRemote().sendText(message);
		} catch (IOException e)
		{
			e.printStackTrace();
		}
	}

}

存在的问题

  1. 并发问题
    每个ChatEndpoint是放在一个线程里运行的,每个线程要并发地访问 clientList 对象
  2. 效率问题
    当用户多、消息多的时候,如何既能保证线程安全、又能及时将消息发送出去。
  3. 用户身份无法确定、发出的消息没法确认是谁发的。
  • 我们使用Synchronized来保证线程安全解决并发问题、但是没有解决效率问题,两者必须先保证线程安全问题。
  • 原因:多个用户同时竞争一个Synchronized锁,直接升级为轻量级锁或者重量级锁,导致效率问题。

用户身份

  • 多人聊天时,每个用户应该有一个名字。
  • 在项目中增加Spring支持;http://127.0.0.1:8080/demo/chatroom
    后台:ChatController.chatroom()
    前端:/template/chatroom.html
  • 在网站应用里,一般是从当前会话HttpSession中获取。所以 需要在Endpoint里,能够获取当前HttpSession

解决方法

  1. 添加 ChatEndpointConfig 类
public class ChatEndpointConfig extends Configurator
{
	@Override
	public void modifyHandshake(ServerEndpointConfig sec
			, HandshakeRequest request
			, HandshakeResponse response)
	{
		// 把 HttpSession 放到 ServerEndpointConfig 中
		HttpSession httpSession = (HttpSession) request.getHttpSession();
		sec.getUserProperties().put("httpSession", httpSession);
	}
}
  1. 指定 Configurator
@ServerEndpoint(value="/websocket/chat", configurator=ChatEndpointConfig.class)
public class ChatEndpoint
{
}
  1. 在 OnOpen 中取得 HttpSession 。即当用户登录成功,进入聊天室建立WebSocket完成后,记录用户的信息;
@OnOpen
	public void onOpen(Session session, EndpointConfig config)
	{		
		// 从 EndpointConfig 获取当前 HttpSession对象
		HttpSession httpSession =(HttpSession)config.getUserProperties().get("httpSession");		
	}

用户姓名的显示代码

客户端登录页面login.html,直接复制即可;


<html>
	<head>
		<meta charset="utf-8">
		<title>在线聊天室title>
		
		<script th:src="@{/js/jquery.min.js}" >script>
		<script th:src="@{/js/afquery.js}" >script>
		<link rel="stylesheet" th:href="@{/css/common.css}" />
		
		<style>
			.container{
				margin: 30px auto auto auto;
				width: 500px;
			}
			.form{
				background:#fcfcfc;
				padding: 10px;
			}
			.line{
				margin: 10px 0;
			}
			.label{
				display:inline-block;
				width: 80px;
			}
			.line input{
				width: 300px;
			}
			
		style>
	head>
	<body>
		<div class='container form' >
			<div class='line'>
				登录聊天室
			div>
			<div class='line'>
				<span class='label'>用户名span>
				<input type='text' class='user' >
			div>
			<div class='line'>
				<span class='label'>密码span>
				<input type='password' class='password' >
			div>
			<div class='line' style='margin-top: 40px'>
				<button onclick='my.login()'> 登录 button>
			div>
		div>
		
	body>

	<script>

		var my = {};
		
	
		// 用户登录
		my.login = function(){
			var req = {};
			req.user = $('.form .user').val().trim();
			req.password = $('.form .password').val().trim();
			
			Af.rest('[[@{/login.do}]]' , req, function(data){
				location.href = '[[@{/chatroom}]]'	;	
			})
		}
		
		
	script>
html>

客户端聊天室页面chatroom.html,直接复制即可;


<html>
	<head>
		<meta charset="utf-8">
		<title>在线聊天室title>
		
		<script th:src="@{/js/jquery.min.js}" >script>
		<script th:src="@{/js/afquery.js}" >script>
		<link rel="stylesheet" th:href="@{/css/common.css}" />
		
		<style>
			.container{
				margin: 30px auto auto auto;
				width: 800px;
			}
			.line{
				margin: 10px 0;
			}
			.message{
				width: calc(100% - 80px);
				padding: 6px;
			}
			.chatroot{
				width: 100%;
				height: 300px;
				font-size: 15px;
				line-height: 130%;
			}
		style>
	head>
	<body>
		<div class='container'>
			
			<div class='line'>
				当前用户: <span th:text='${session.user}' style='color:blue'>span>
			div>
			<div class='line'>
				<input type='text' class='message'>
				<button onclick='my.doSend()'> 发送 button>
			div>
									
			<div class='line'>
				<textarea class='chatroot'>textarea>
			div>
		div>
		
	body>

	<script>
		
		// 打开WebSocket
		// ws://127.0.0.1:8080/demo/websocket/chat
		var sock = new WebSocket("ws://" + location.host + "/demo/websocket/chat");
	
		// 指定回调方法
		sock.onopen = function() {
			console.log('Client Socket Open');
		};
		sock.onclose = function(event) {
			console.log('Client Socket Closed');
		};
		sock.onerror = function(event) {
			console.log('Client Socket Error');
		};
		sock.onmessage = function(event) {
			console.log('** Client Receive:' + event.data);
			my.showMessage(event.data);
		};

		var my = {};
		
		// 发送
		my.doSend = function(){
			var message = $('.message').val();
			// my.showMessage('>> ' + message );
			$('.message').val(''); // 清空输入
			
			// 发送消息
			sock.send( message );		
		}
		
		// 显示消息
		my.showMessage = function(message){
			$('.chatroot').append(message + '\n');
		}
		
	
	script>
html>

登录验证类LoginController

package my;

import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import com.alibaba.fastjson.JSONObject;

import af.spring.AfRestData;

@Controller
public class LoginController
{
	@GetMapping("/login")
	public String login()
	{
		return "login";
	}
	
	@PostMapping("/login.do")
	public Object login(@RequestBody JSONObject jreq
			, HttpSession session) throws Exception
	{
		// 登录
		String user = jreq.getString("user");
		session.setAttribute("user", user);
		
		return new AfRestData("");
	}	
}

服务类EndPoint

package my;

import java.io.IOException;

import javax.servlet.http.HttpSession;
import javax.websocket.EndpointConfig;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint(value="/websocket/chat", configurator=ChatEndpointConfig.class)
public class ChatEndpoint
{
	Session session; // 一个WebSocket连接,不是 HttpSession
	String user; // 当前用户身份,考虑从 HttpSession中获取
	
	public ChatEndpoint()
	{
		System.out.println("** 创建一个Endpoint...");
	}
	
	@OnOpen
	public void onOpen(Session session, EndpointConfig config)
	{		
		this.session = session;
		ChatRoom.i.addClient(this);
		System.out.println("** 新的用户连接: ");
		
		// 从 EndpointConfig 获取当前 HttpSession对象
		HttpSession httpSession = (HttpSession)config.getUserProperties().get("httpSession");
		this.user = (String)httpSession.getAttribute("user");
		if(user == null) user = "未登录";
	}

	@OnClose
	public void onClose()
	{	
		ChatRoom.i.removeClient(this);
		System.out.println("** 用户连接已关闭");
	}

	@OnError
	public void onError(Session session, Throwable error)
	{
		ChatRoom.i.removeClient(this);
		System.out.println("** 用户连接出错");
		error.printStackTrace();
	}
	
	@OnMessage
	public void onMessage(String message, Session session)
	{
		System.out.println("用户消息:" + message);
		
		ChatRoom.i.sendAll(user + ": " + message);
	}

	public void sendMessage(String message)
	{
		try
		{
			session.getBasicRemote().sendText(message);
		} catch (IOException e)
		{
			e.printStackTrace();
		}
	}

}

配置类Configurator:

public class ChatEndpointConfig extends Configurator
{
	@Override
	public void modifyHandshake(ServerEndpointConfig sec
			, HandshakeRequest request
			, HandshakeResponse response)
	{
		// 把 HttpSession 放到 ServerEndpointConfig 中
		HttpSession httpSession = (HttpSession) request.getHttpSession();
		sec.getUserProperties().put("httpSession", httpSession);
	}
}

聊天室类ChatRoom.java

package my;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class ChatRoom
{
	// 创建全局实例  ChatRoom.i 
	public static ChatRoom i = new ChatRoom();
	
	// 客户端的列表
	private List<ChatEndpoint> clientList = new LinkedList<>();
		
	// 当有客户端连接时,将此客户端添加到 clientList
	public void addClient(ChatEndpoint client)
	{
		synchronized (clientList)
		{
			clientList.add(client);
		}
	}
	
	// 当有客户端断开时,将此客户端从 clientList 移除
	public void removeClient(ChatEndpoint client)
	{
		synchronized (clientList)
		{
			clientList.remove(client);
		}
	}
	
	// 发给所有人
	public void sendAll(String message)
	{		
		// 1 并发问题
		// 2 效率问题
		synchronized (clientList)
		{
			Iterator iter = clientList.iterator();
			while(iter.hasNext())
			{
				ChatEndpoint client = (ChatEndpoint)iter.next();
				client.sendMessage( message );
			}
		}		
	}
}

至此聊天室代码基本完工了。剩下效率问题没搞定;

并发优化

  • 并发问题:假设1000人在访问这个聊天室
  • 有的人正在进入 clientList.add()
  • 有的人正在断开 clientList.remove()
  • 有的人正在发送消息 clientList.iterator()
  • 多个线程,最终都要并发地访问同一个对象
    ChatRoom.i.clientList

分析用户的操作:

  • 用户的加入与断开,是个低频操作
  • 消息的群发,是个高频、高耗时的操作

这种场景就是典型的读多写少,且数据一致性不会那么严苛。

解决方法

  • 针对读夺写少场景,可以使用CopyOnWriteArrayList线程安全类;
  • 参照读写分离思想,让写操作对应一个对象,让读操作对应一个拷贝对象;
package my;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class ChatRoom
{
	// 创建全局实例  ChatRoom.i 
	public static ChatRoom i = new ChatRoom();
	
	// 客户端的列表
	private List<ChatEndpoint> clientList = new LinkedList<>();
		
	private List<ChatEndpoint> clientListCopy = new LinkedList<>();;
	
	// 当有客户端连接时,将此客户端添加到 clientList
	public void addClient(ChatEndpoint client)
	{
		synchronized (clientList)
		{
			clientList.add(client);
			
			// 生成一份新的拷贝
			clientListCopy = new LinkedList<>( clientList);
		}
	}
	
	// 当有客户端断开时,将此客户端从 clientList 移除
	public void removeClient(ChatEndpoint client)
	{
		synchronized (clientList)
		{
			clientList.remove(client);
			
			// 生成一份新的拷贝
			clientListCopy = new LinkedList<>( clientList);
		}
	}
	
	// 发给所有人
	public void sendAll(String message)
	{		
		// 1 并发问题
		// 2 效率问题
		Iterator iter = clientListCopy.iterator();
		while(iter.hasNext())
		{
			ChatEndpoint client = (ChatEndpoint)iter.next();
			client.sendMessage( message );
		}
				
	}
}

聊天室代码已经上传到资源里,想要的就去下载。

你可能感兴趣的:(Java基础,网络编程,java)