一、实验描述
本实验实现基于TCP的多人聊天室。多个用户同时访问服务端,各自可以不断请求服务端获取响应的数据,并可以实现群聊和私聊功能。服务器则实现对数据的转发功能。
本文工具:IDEA2020、JDK1.8
本文源代码在文章末尾
二、分析
1.客户端
(1)数据发送
(2)数据接收
技术:socket、输入流和输出流、多线程
聊天:群聊、私聊
注意:私聊格式:@服务器用户ID号:msg
2.服务端
(1)数据转发
(2)用户注册
技术:ServerScoket、每个用户对应Socket对象、多线程同时在线
数据转发:私聊前缀判断、群聊所有人发送
三、创建客户端
创建完客户端结构如下:

(1)创建基础项目
打开IDEA,点击File—>New—>Project

下面这里选择Java,然后右侧选择SDK版本,这里选择1.8版本

下一步输入Project name,这里 项目名称为 ClientWithTCP,右下角选择“Finish”
右击src文件夹,选择New—>Package

这里创建一个包,包名为com.guo.www (或者其他包名)

(2)创建工具类
主要用于结束所有的连接。
右击www文件夹,选择New—>Java Class

名字为ChatUtils,参考代码如下:
import java.io.Closeable;
import java.io.DataOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.net.Socket;
public class ChatUtils {
/**
* 安全关闭多个资源(支持可变参数,自动处理null和异常)
* @param resources 可变参数,可以是 DataOutputStream、DataInputStream、BufferedReader、Socket 等
*/
public static void close(AutoCloseable... resources) {
if (resources == null) {
return;
}
for (AutoCloseable resource : resources) {
try {
if (resource != null) {
resource.close();
}
} catch (Exception e) {
System.err.println("关闭资源时发生异常: " + e.getMessage());
}
}
}
}
(3)编写客户端发送线程类
右击www文件夹,选择New—>Java Class

名字为SendThreadClient

参考代码如下:
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
public class SendThreadClient implements Runnable{
private BufferedReader reader;
private DataOutputStream dos;
private Socket clientSocket;
private boolean isRunning;
private String name;
public SendThreadClient(Socket client,String name){
this.clientSocket=client;
this.isRunning=true;
this.name=name;
reader=new BufferedReader(new InputStreamReader(System.in));
try {
dos=new DataOutputStream(client.getOutputStream());
//发送用户给服务器,用户名用于注册,需要调用send方法
send(name);
}catch (IOException ex){
ex.printStackTrace();
//释放资源
release();
}
}
/**
*
* 线程代码,只要当前连接状态,就一直读取字符串和发送信息
* */
@Override
public void run(){
while (isRunning){
String msg = null;
try {
msg=reader.readLine();
}catch (IOException e){
System.out.println("数据写入失败"+e.toString());
release();
}
send(msg);
}
}
/***
*
* @param msg 需要发送的信息
*/
public void send(String msg){
try {
System.out.println("本机发送信息:"+msg);
dos.writeUTF(msg);
dos.flush();
}catch (IOException e){
System.out.println("数据发送失败"+e.toString());
release(); //释放资源
}
}
//释放资源
public void release(){
ChatUtils.close(dos,clientSocket,reader);
}
}
(4)编写客户端接收线程类
右击www文件夹,选择New—>Java Class

名字为ReceiveThreadClient,
参考代码如下:
import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;
public class ReceiveThreadClient implements Runnable{
private boolean isRunning;
private DataInputStream dis;
private Socket client;
/**
* 构造方法
* */
public ReceiveThreadClient(Socket client){
this.client=client;
this.isRunning=true;
try {
dis= new DataInputStream(client.getInputStream());
}catch (IOException ex){
System.out.println("DataInputStream对象创建失败");
release();
}
}
@Override
public void run(){
while (isRunning){
String msg = "";
msg=receive();
if(!msg.equals("")){
System.out.println(msg);
}
}
}
/***
* 从服务端接收数据
* @return
*/
public String receive(){
String msg=null;
try {
msg=dis.readUTF();
return msg;
}catch (IOException e){
System.out.println("数据接收失败");
release();
}
return "";
}
//释放资源
public void release(){
isRunning=false;
ChatUtils.close(dis,client);
}
}
(5)编写客户端类
右击www文件夹,选择New—>Java Class

名字为ClientMultiUser

参考代码如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ClientMultiUser {
static String ServerIP="127.0.0.1";
static int Port=8888;
public static void main (String[] args) throws IOException{
System.out.println("TCP客户端启动");
System.out.println("请输入用户名:");
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String name = reader.readLine(); //控制台输入数据
//创建Socket,绑定服务器IP地址和端口
Socket client = new Socket(ServerIP,Port);
//使用线程池管理两个线程,一个是发送线程,一个是接收线程
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(new SendThreadClient(client,name)); //发送线程
pool.submit(new ReceiveThreadClient(client)); //接收线程
}
}
四、创建服务端
服务端建议重新创建一个新项目,单独运行。项目名称为ReceiveWithTCP

前面的都一样,创建包和ChatUtils工具类,整个项目结构如下:

右击www文件,选择New—>Java Class
类名为ServerMultiUser,参考代码如下:
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.CopyOnWriteArrayList;
public class ServerMultiUser {
//用于存储所有客户端的一个容器,涉及多线程的并发操作
//使用CopyOnWriteArrayList保证线程安全
private static CopyOnWriteArrayList<Channel> allClient= new CopyOnWriteArrayList<Channel>();
public static void main(String[] args) throws Exception{
System.out.println("服务端启动");
//建立ServerSocket对象,并绑定本地端口
ServerSocket server = new ServerSocket(8888);
//一直循环,接收来自多个客户端的请求
while (true){
//监听
Socket clientSocket = server.accept();
Channel client = new Channel(clientSocket);
allClient.add(client);
System.out.println(client.name+"建立了连接");
new Thread(client).start();
}
}
static class Channel implements Runnable {
private DataInputStream dis;
private DataOutputStream dos;
private Socket client;
private boolean isRunning;
private String name;
//构造方法
public Channel(Socket client){
this.client = client;
this.isRunning = true;
try{
dis = new DataInputStream(client.getInputStream());
dos=new DataOutputStream(client.getOutputStream());
this.name=reveive();//接收客户端的名称
this.send("欢迎来到聊天室。。。");
this.sendOther(this.name+"来到了聊天室。。。",true);
}catch (IOException e){
release(); //释放资源
}
}
@Override
public void run(){
while (isRunning){
String msg= reveive();
if(!msg.equals("")){
sendOther(msg,false);
}
}
}
//发送数据
public void send(String msg){
try {
dos.writeUTF(msg);
dos.flush();
}catch (IOException e){
System.out.println("数据发送失败");
release();
}
}
/**
* 获取自己的消息,然后发送给其他人
* isSys 表示是否为系统消息
* 私聊:可以向某一特定的用户发送数据
* 约定格式:@服务器用户ID:消息
*/
public void sendOther(String msg,boolean isSys){
boolean isPrivate = msg.startsWith("@");
if(isPrivate){
//寻找冒号
int index = msg.indexOf(":");
//截取ID
String targetName = msg.substring(1,index);
//截取消息内容
String datas = msg.substring(index+1);
for(Channel other:allClient){
if(other.name.equals(targetName)){
other.send(this.name+"悄悄对你说:"+datas);
}
}
}else {
for(Channel other:allClient){
if(other==this){
//自己不能给自己发
continue;
}
if(!isSys){
other.send(this.name+"对大家说:"+msg);
}else{
other.send(msg);
}
}
}
}
//接收数据
public String reveive(){
try {
String msg = "";
msg=dis.readUTF();
return msg;
}catch (IOException e){
isRunning =false;
System.out.println("接收数据失败");
release();
}
return "";
}
//释放资源
public void release(){
this.isRunning=false;
ChatUtils.close();
allClient.remove(this);
this.sendOther(this.name+"离开了聊天室...",true);
}
}
}
五、测试效果
先启动服务端,然后复制两个客户端,单独启动,分别在客户端发送数据,观察其他客户端接收数据的情况
最左侧是服务端,中间是第一个客户端,最右边是第二个客户端
效果如下:

服务端源码(访问密码: 5241):ReceiveWithTCP.zip: https://url47.ctfile.com/f/64055047-1508351275-1f6e0f?p=5241
客户端源码(访问密码: 6423):ClientWithTCP.zip: https://url47.ctfile.com/f/64055047-1508351575-000eff?p=6423