日期: 11/20/2021
上一篇
Java语言实战开发 @网络聊天室 (一) 简单实现用户发消息给后台管理员
1. 探索服务端接收消息的机制
在上一次的实践中,使用缓冲输入输出流通过套接字进行网络通信,但是目前只能实现使用客户端发送消息给服务端,服务端无法做出回应,这一次就对此问题进行优化。
由于是初次使用
ServerSocket
类,有一些机制需要自己探索,比如在Server中这段:
while(true){
Socket accept = serverSocket.accept();
BufferedReader r = new BufferedReader(new InputStreamReader(accept.getInputStream()));
String message;
while((message = r.readLine()) != null)
System.out.println("收到: " + message);
最外层使用一个while循环不断执行,接收客户端套接字的程序,同时根据套接字的字节输入流而创建缓冲输入流,进而读取来自客户端的数据,那么服务端最终会执行多少次 while (true) 呢?
是否在客户端发送一次消息过后,服务端就结束一次while循环了呢?只需一个变量就可以解决这个疑问,测试代码如下:
int i = 0;
while(true){
// ... 前面部分和之前一样
System.out.println("-----" + (++i));
接下来先后打开服务端、客户端,并在客户端发送三条测试消息,测试结果如下:
答案是:while (true) 只执行了一次,而且一直没有结束.。
这意味着服务端主线程一直在执行 while(message = r.rindLine() != null);
的语句。
现在把while 改成 if ,同时把socket的获取放在前面,否则会一直阻塞进程,测试效果如下:
Socket accept = serverSocket.accept();
while(true){
//...
if(message=r.rindLine())!=null){
// ...
成功解决了服务端接受一个客户端的线程阻塞问题,接下来测试两个客户端,通过修改端口类的连接IP实现。
很明显,由于 服务端获取socket的代码必须放在外面,否则无法一直执行while(true) 的所有内容(线程阻塞),但是在主线程,服务端必须要随时等待客户端发来的消息,此时也有其他客户端连接的可能,所以这里需要引用多线程的机制,使用Java.lang
包下的Thread
类实现该过程。
2 多线程机制的应用
实现多线程的方法主要有两种
方式一:实现Runnable接口,重写Run方法
class RunnableImpl implements Runnable{
@Override
public void run(){
// ...
// 使用通过 new Thread(new RunnableImpl()).start()
方式二:继承Thread类,重写Run方法
class myThread extends Thread{
@Override
public void run(){
// ...
// 使用通过 new myThread().start();
它们的区别是:
实现接口方式,还可以继承其他的类,具有更高的扩展性,但只有多线程的run方法。
继承的方式,具有原生Thread的方法,更加全面,但扩展性低,无法再继承第二个类。
当有多个客户端连接服务端时,即多个用户需要管理员进行管理,对于每个用户,管理员需要进行不同的约束,当然也有共性,在程序实现中,给每个用户分配一个线程,这样管理员就不用一一的对用户进行管理,提高了并发性。
现对服务端添加两个线程类,一是负责随时接待连接的客户端,二是针对响应每个客户端的消息。
ServerReadThread.java
该类负责处理一个客户端的消息发送,其中的socket
对象是与服务端连接时创建的socket,所以可以根据该对象进行数据通信。
package App;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;
public class ServerReadThread extends Thread{
private Socket socket;
private DataInputStream in;
public ServerReadThread(Socket socket){
this.socket = socket;
try {
in = new DataInputStream(socket.getInputStream());
} catch (IOException e) {
System.out.println("获取套接字输入流对象失败");
@Override
public void run() {
while (true) {
String info;
try {
if ((info = in.readUTF()) != null)
System.out.println(info);
} catch (IOException e) {
e.printStackTrace();
ServerReadyThread.java
该类负责处理每个客户端的连接,其中的serverSocket
对象是创建服务端时产生的套接字,通过该对象可以调用accept()
方法接收到连接的客户端套接字。
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerReadyThread extends Thread{
private ServerSocket serverSocket;
public ServerReadyThread(ServerSocket serverSocket){
this.serverSocket = serverSocket;
@Override
public void run() {
while (true){
try {
Socket accept = serverSocket.accept();
new ServerReadThread(accept).start();
Server.list_socket.add(accept);
} catch (IOException e) {
e.printStackTrace();
Server.java
该类是服务端的主要实现类,采用多线程过后,主线程可以不用while(true)循环,
其中的 list_socket
存储当前连接的所有客户端套接字,在客户端连接时会添加进来,这样方便主线程可以对客户端进行消息反馈。
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
public class Server {
private final static int port = 6666;
public final static ArrayList<Socket> list_socket = new ArrayList<>(5);
public static void main(String[] args) throws IOException {
System.out.println("服务端启动");
int i = 0;
ServerSocket serverSocket = new ServerSocket(port);
// 启动负责连接的线程
new ServerReadyThread(serverSocket).start();
3. 服务端的反馈机制
通过多线程机制,现在服务端的主线程已经可以继续执行其他的操作,比如再添加更多的多线程。
之前所有客户端套接字存储在了 list_sockets
对象里,既然是相通的套接字,那么服务端同样可以发送消息给出去,只要客户端也做相应的接收即可。
再次使用多线程机制,这次在客户端类设计多线程负责接收来自服务端的消息,同时在服务端添加发送消息的代码。
Server.java
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Scanner;
public class Server {
private final static int port = 6666;
public final static ArrayList<Socket> list_socket = new ArrayList<>(5);
public static Scanner in = new Scanner(System.in);
public static void main(String[] args) throws IOException {
System.out.println("服务端启动");
int i = 0;
ServerSocket serverSocket = new ServerSocket(port);
// 启动负责连接的线程
new ServerReadyThread(serverSocket).start();
tips();
while (true) {
String operation = in.nextLine();
switch (operation){
case "1":
sendMsgToClient();
break;
default:
System.out.print(">> 不支持当前指令,请重新输入\n>> ");
tips();
public static void tips(){
System.out.println("==========欢迎访问服务端========");
System.out.println("| 现在可进行的操作如下 |");
System.out.println("| 1. 向指定客户端发送消息 |");
System.out.println("================================");
System.out.printf(">> ");
public static void sendMsgToClient(){
showAllClient();
System.out.print(">> 请选择要发送到的客户端(e可取消操作)\n>> ");
boolean flag = true;
while (flag) {
String choice = in.nextLine();
switch (choice) {
case "e":
flag = false;
break;
default:
int i = Integer.parseInt(choice) -1;
if(i < list_socket.size()){
System.out.print(">> 选择成功, 接下来请输入要发送的内容\n>> ");
try {
DataOutputStream out = new DataOutputStream(list_socket.get(i).getOutputStream());
out.writeUTF(in.nextLine());
System.out.println(">> 消息发送成功!");
flag = false;
} catch (IOException e) {
e.printStackTrace();
} else{
System.out.println("选择的客户端不存在, 当前客户端总数目为: " + list_socket.size());
public static void showAllClient(){
for (int i = 0; i < list_socket.size(); i++) {
SocketAddress name = list_socket.get(i).getRemoteSocketAddress();
System.out.printf("%d号客户端:%s\n", (i+1), name);
ClientReadServer.java
该类负责处理客户端接收套接字的消息
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
public class ClientReadThread extends Thread{
private Socket socket;
private DataInputStream in;
public ClientReadThread(Socket socket){
this.socket = socket;
try {
in = new DataInputStream(socket.getInputStream());
} catch (IOException e) {
System.out.println("获取套接字输出流失败.");
@Override
public void run() {
while(true){
String info;
try {
if((info = in.readUTF())!=null)
System.out.println("服务端发送了:" + info);
} catch (IOException e) {
e.printStackTrace();
最后需要在 Client.java
中添加 new ClientReadThread(socket).start();
确保线程启动。
测试结果:
4. 实现注册登陆功能
通过上面的测试,可以发现在客户端发送消息时,并不知道自己的名称,这显然不符合用户的定义,聊天工具一般都有个人的账号系统,所以在服务端和客户端连接前,最好指定一个登陆状态对象,并且服务端可以通过该对象读取已连接客户端的信息,类似于对上线的用户进行的操作。
PersonInfo.java
用户信息实体类
存储管理员和用户两个身份的信息,主要包括登陆ID、登陆密码和身份
public class PersonInfo {
private String id;
private String pw;
private String identity;
private boolean isLogin = false;
public PersonInfo() {};
public PersonInfo(String id, String pw, String identity) {
this.id = id;
this.pw = pw;
this.identity = identity;
public String getId() {
return id;
public void setId(String id) {
this.id = id;
public String getPw() {
return pw;
public void setPw(String pw) {
this.pw = pw;
public String getIdentity() {
return identity;
public void setIdentity(String identity) {
this.identity = identity;
public void setLogin(boolean x){
this.isLogin = x;
Login.java
登陆功能实现类
该类可以通过判断账号ID与密码是否匹配而实现登陆,若账号不存在则可以直接创建,若密码错误则提示重新输入,代码看起来比较简洁,因为大多是调用自定义的数据工具类DataUtils.java
的里的静态方法
import java.io.*;
import java.util.ArrayList;
import java.util.Scanner;
public class Login {
public static Scanner in = new Scanner(System.in);
public static PersonInfo go() {
System.out.println("========登陆系统=======");
PersonInfo user = new PersonInfo(null, null, "admin");
boolean needLogin = true;
while (needLogin) {
System.out.print("id>> "); user.setId(in.nextLine()) ;
System.out.print("pw>> "); user.setPw(in.nextLine());
if (DataUtils.checkPersonInfo(user) != null) {
System.out.println(">> 登陆成功!");
return user;
} else if (DataUtils.checkId(user.getId()) == false) {
System.out.println(">> 密码错误");
} else {
System.out.println(">> 该用户不存在,是否创建?(y/n)");
switch (in.nextLine()) {
case "y": ;
DataUtils.writeRegister(user);
System.out.println(">> 已登陆.");
needLogin = false;
break;
default:
System.out.println(">> 请重新登陆");
break;
return user;
DataUtils.java
数据工具类
该类存储了所有的登陆用户的信息、以及连接的所有套接字,并提供一些相关的操作,比如读取注册用户,写入用户,验证登陆等。
import java.io.*;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.ArrayList;
public class DataUtils {
// 变量: 存储所有的注册用户 (包括管理员下同)
private final static ArrayList<PersonInfo> list_origin_info = new ArrayList<>();
// 变量: 存储所有上线的用户
private final static ArrayList<PersonInfo> list_online_info = new ArrayList<>();
// 变量: 存储所有的客户端与服务端的套接字
public final static ArrayList<Socket> list_socket = new ArrayList<>(5);
// 功能: 从本地读取所有的注册用户
public static void readRegister(){
try {
BufferedReader in = new BufferedReader(new FileReader("personInfo.txt"));
String info;
while ((info = in.readLine()) != null) {
String[] split = info.split(",");
if (split != null && split.length == 3)
list_origin_info.add(new PersonInfo(split[0], split[1], split[2]));
} catch (IOException e) { e.printStackTrace();}
// 功能: 返回所有注册用户
public static ArrayList<PersonInfo> getOriginInfo() {return list_origin_info; }
// 功能: 将新用户写入配置文件
public static void writeRegister(PersonInfo p){
try {
BufferedWriter out = new BufferedWriter(new FileWriter("personInfo.txt", true));
out.write(p.getId() + "," + p.getPw() + "," + p.getIdentity() + "\n");
out.flush();
p.setLogin(true);
list_online_info.add(p);
System.out.println(p.getId() + " 注册成功!");
} catch (IOException e) {
e.printStackTrace();
// 功能: 返回所有线上用户
public static ArrayList<PersonInfo> getOnlienInfo() {return list_online_info; }
// 功能: 检查ID是否存在
public static boolean checkId(String id){
for (PersonInfo personInfo : list_origin_info) {
if (id.equals(personInfo.getId()))
return false;
return true;
// 功能: 显示所有客户端的名称
public static void showAllClient(){
for (int i = 0; i < list_socket.size(); i++) {
SocketAddress name =list_socket.get(i).getRemoteSocketAddress();
System.out.printf("%d号客户端:%s\n", (i+1), name);
// 功能: 检查用户是否存在,即验证登陆正确性
public static PersonInfo checkPersonInfo(PersonInfo user){
readRegister();
String id = user.getId();
String pw = user.getPw();
String identity = user.getIdentity();
for (PersonInfo p : DataUtils.getOriginInfo()) {
if (id.equals(p.getId()) && pw.equals(p.getPw()) &&
identity.equals(p.getIdentity())) {
PersonInfo pi = new PersonInfo(id, pw, identity);
pi.setLogin(true);
return pi;
return null;
Server.java
服务端类, 部分有修改
public class Server {
//...
private static PersonInfo admin = null;
public static void main(String[] args) throws IOException {
admin = Login.go();
//...
while (true) {
//...
5. 总结 、所有实现代码
5.1 项目结构
项目结构:
各种类的作用描述表
5.2 存在的不足
IO流的选择问题,在上一篇中,笔者用到了 BufferedReader
字符缓冲输入流,适合字符数据的传输,另外,DataInputStream
似乎更适合数据的传输,但是又有一个缺点,它必须遵循严格的读写类型,比如若在socket的一端是调用readInt()
那么另一端就必须是 writeInt()
,否则会抛出异常,同时还有ObjectInputStream
这类输入流存在,可以尝试一番。
项目结构层次比较混乱,没有接口,未体现出多态的特性。
未考虑到多线程机制的安全性问题,目前只是按需求创建线程,以便处理事件,还没有考虑到线程是否能够合理地使用系统资源。
5.3 所有实现代码
Client.java
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class Client {
private static PersonInfo user = null;
private static Socket socket;
public static void main(String[] args) throws IOException {
System.out.println("客户端启动");
user = Login.go();
// 连接服务端
socket = new Socket("localhost", 6666);
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
// 启动接收服务端消息的线程
new ClientReadThread(socket).start();
Scanner in = new Scanner(System.in);
out.writeUTF("\n>> " + user.getId() + " 已加入聊天室!");
while(true){
System.out.print(user.getId() + " >> ");
String info = in.nextLine();
out.writeUTF("\n>> 收到来自 [ " + user.getId() +" ] 的消息: " + info);
ClientReadThread.java
import java.io.*;
import java.net.Socket;
public class ClientReadThread extends Thread{
private Socket socket;
private DataInputStream in;
public ClientReadThread(Socket socket){
this.socket = socket;
try {
in = new DataInputStream(socket.getInputStream());
} catch (IOException e) {
System.out.println("获取套接字输出流失败.");
@Override
public void run() {
while(true){
String info;
try {
if((info = in.readUTF())!=null)
System.out.println("\n服务端发送了:" + info);
} catch (IOException e) {
e.printStackTrace();
DataUtils.java
import java.io.*;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.ArrayList;
* @author Uni
* @create 2021/11/20 10:55
public class DataUtils {
// 变量: 存储所有的注册用户 (包括管理员下同)
private final static ArrayList<PersonInfo> list_origin_info = new ArrayList<>();
// 变量: 存储所有上线的用户
private final static ArrayList<PersonInfo> list_online_info = new ArrayList<>();
// 变量: 存储所有的客户端与服务端的套接字
public final static ArrayList<Socket> list_socket = new ArrayList<>(5);
// 功能: 从本地读取所有的注册用户
public static void readRegister(){
try {
BufferedReader in = new BufferedReader(new FileReader("personInfo.txt"));
String info;
while ((info = in.readLine()) != null) {
String[] split = info.split(",");
if (split != null && split.length == 3)
list_origin_info.add(new PersonInfo(split[0], split[1], split[2]));
in.close();
} catch (IOException e) { e.printStackTrace();}
// 功能: 返回所有注册用户
public static ArrayList<PersonInfo> getOriginInfo() {return list_origin_info; }
// 功能: 将新用户写入配置文件
public static void writeRegister(PersonInfo p){
try {
BufferedWriter out = new BufferedWriter(new FileWriter("personInfo.txt", true));
out.write(p.getId() + "," + p.getPw() + "," + p.getIdentity() + "\n");
out.flush();
p.setLogin(true);
list_online_info.add(p);
System.out.println(p.getId() + " 注册成功!");
out.close();
} catch (IOException e) {
e.printStackTrace();
// 功能: 返回所有线上用户
public static ArrayList<PersonInfo> getOnlienInfo() {return list_online_info; }
// 功能: 检查ID是否存在
public static boolean checkId(String id){
for (PersonInfo personInfo : list_origin_info) {
if (id.equals(personInfo.getId()))
return false;
return true;
// 功能: 显示所有客户端的名称
public static void showAllClient(){
for (int i = 0; i < list_socket.size(); i++) {
SocketAddress name =list_socket.get(i).getRemoteSocketAddress();
System.out.printf("%d号客户端:%s\n", (i+1), name);
// 功能: 检查用户是否存在,即验证登陆正确性
public static PersonInfo checkPersonInfo(PersonInfo user){
readRegister();
String id = user.getId();
String pw = user.getPw();
String identity = user.getIdentity();
for (PersonInfo p : DataUtils.getOriginInfo()) {
if (id.equals(p.getId()) && pw.equals(p.getPw()) &&
identity.equals(p.getIdentity())) {
PersonInfo pi = new PersonInfo(id, pw, identity);
pi.setLogin(true);
return pi;
return null;
Login.java
import java.io.*;
import java.util.ArrayList;
import java.util.Scanner;
public class Login {
public static Scanner in = new Scanner(System.in);
public static PersonInfo go() {
System.out.println("========登陆系统=======");
PersonInfo user = new PersonInfo(null, null, "admin");
boolean needLogin = true;
while (needLogin) {
System.out.print("id>> "); user.setId(in.nextLine()) ;
System.out.print("pw>> "); user.setPw(in.nextLine());
if (DataUtils.checkPersonInfo(user) != null) {
System.out.println(">> 登陆成功!");
return user;
} else if (DataUtils.checkId(user.getId()) == false) {
System.out.println(">> 密码错误");
} else {
System.out.println(">> 该用户不存在,是否创建?(y/n)");
switch (in.nextLine()) {
case "y": ;
DataUtils.writeRegister(user);
System.out.println(">> 已登陆.");
needLogin = false;
break;
default:
System.out.println(">> 请重新登陆");
break;
return user;
PersonInfo.java
public class PersonInfo {
private String id;
private String pw;
private String identity;
private boolean isLogin = false;
public PersonInfo() {};
public PersonInfo(String id, String pw, String identity) {
this.id = id;
this.pw = pw;
this.identity = identity;
public String getId() {
return id;
public void setId(String id) {
this.id = id;
public String getPw() {
return pw;
public void setPw(String pw) {
this.pw = pw;
public String getIdentity() {
return identity;
public void setIdentity(String identity) {
this.identity = identity;
public void setLogin(boolean x){
this.isLogin = x;
Server.java
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Scanner;
public class Server {
private final static int port = 6666;
private static PersonInfo admin = null;
public static Scanner in = new Scanner(System.in);
public static void main(String[] args) throws IOException {
System.out.println("服务端启动");
admin = Login.go();
int i = 0;
ServerSocket serverSocket = new ServerSocket(port);
// 启动负责连接的线程
new ServerReadyThread(serverSocket).start();
tips();
while (true) {
String operation = in.nextLine();
switch (operation) {
case "1":
sendMsgToClient();
break;
default:
System.out.print(">> 不支持当前指令,请重新输入\n>> ");
tips();
public static void tips() {
System.out.println("==========欢迎访问服务端========");
System.out.println("| 现在可进行的操作如下 |");
System.out.println("| 1. 向指定客户端发送消息 |");
System.out.println("================================");
System.out.printf(">> ");
public static void sendMsgToClient() {
DataUtils.showAllClient();
System.out.print(">> 请选择要发送到的客户端(e可取消操作)\n>> ");
boolean flag = true;
while (flag) {
String choice = in.nextLine();
switch (choice) {
case "e":
flag = false;
break;
default:
int i = Integer.parseInt(choice) - 1;
if (i < DataUtils.list_socket.size()) {
System.out.print(">> 选择成功, 接下来请输入要发送的内容\n>> ");
try {
DataOutputStream out = new DataOutputStream(DataUtils.list_socket.get(i).getOutputStream());
while (true){
String info = in.nextLine();
if(!info.equals("e")){
out.writeUTF(info);
System.out.print(">> 消息发送成功!\n>> ");
else break;
flag = false;
} catch (IOException e) {
e.printStackTrace();
} else {
System.out.println("选择的客户端不存在, 当前客户端总数目为: " + DataUtils.list_socket.size());
ServerReadThread.java
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
public class ServerReadThread extends Thread{
private static Socket socket;
private static DataInputStream in = null;
public ServerReadThread(Socket socket){
this.socket = socket;
try {
in = new DataInputStream(this.socket.getInputStream());
} catch (IOException e) {
System.out.println("获取套接字输入流对象失败");
@Override
public void run() {
while (true) {
try {
String info;
if (in != null && (info = in.readUTF()) != null)
System.out.println(info);
} catch (IOException e) {
e.printStackTrace();
ServerReadyThread.java
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerReadyThread extends Thread{
private static ServerSocket serverSocket;
public ServerReadyThread(ServerSocket serverSocket){
this.serverSocket = serverSocket;
@Override
public void run() {
while (true){
try {
Socket accept = serverSocket.accept();
new ServerReadThread(accept).start();
DataUtils.list_socket.add(accept);
} catch (IOException e) {
e.printStackTrace();