Java 网络编程 -- 基于TCP 实现聊天室 群聊 私聊

分析:
聊天室需要多个客户端和一个服务端。
服务端负责转发消息。
客户端可以发送消息、接收消息。

消息分类:
	群聊消息:发送除自己外所有人
	私聊消息:只发送@的人
	系统消息:根据情况分只发送个人和其他人
技术方面:
	客户端和服务端收发消息,需要使用IO流,封装一个IOUtils工具类用来释放资源。
	客户端需要同时收发消息,需要启动发送和接收两个消息,互不干扰
	服务端需要接收每个客户端消息和对多个客户端发送消息,每连接上一个客户端需要启动一个线程,让后面进来的客户端不需要等待前面的客户端退出后才能建立连接。

……

还是上代码吧。

基础版:

搭建结构,实现多个客户端和服务端连接,保证服务端能正常转发消息。
我们约定:
当服务端在初始化、发送、接收时出现异常时分别输出:
------1------
------2------
------3------

当客户端,初始化发送线程、初始化接收线程、发送、接收异常时分别输出:
======1=====
======2=====
======3=====
======4=====

1、IO工具类

package com.xzlf.chat;

import java.io.Closeable;
import java.io.IOException;

/**
 * 工具类
 * @author xzlf
 *
 */
public class IOUtils {
	/**
	 * 释放资源
	 * @param closeables
	 */
	public static void close(Closeable...closeables) {
		for (Closeable closeable : closeables) {
			if(null != closeable) {
				try {
					closeable.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

2、服务端

package com.xzlf.chat;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 聊天室:服务器
 * @author xzlf
 *
 */
public class TMultiChat {
	public static void main(String[] args) throws IOException {
		System.out.println("======server======");
		// 1、指定端口创建服务端
		ServerSocket server = new ServerSocket(8888);
		while(true) {
			// 2、每进来一个客户端启动一个线程
			Socket socket = server.accept();
			System.out.println("一个客户端建立了连接");
			new Thread(new Channel(socket)).start();
		}
	}
	
	// 一个Channel 代表一个客户端
	static class Channel implements Runnable{
		private Socket socket;
		private DataInputStream dis;
		private DataOutputStream dos;
		private boolean isRuning;
		public Channel(Socket socket) {
			this.socket = socket;
			this.isRuning = true;
			try {
				dis = new DataInputStream(socket.getInputStream());
				dos = new DataOutputStream(socket.getOutputStream());
			} catch (IOException e) {
				System.out.println("------1------");
				this.release();
			}
			
		}
		
		// 接收消息
		private String receive() {
			String msg = "";
			try {
				msg = dis.readUTF();
			} catch (IOException e) {
				System.out.println("------3------");
				this.release();
			}
			return msg;
		}
		
		// 发送消息
		private void send(String msg) {
			try {
				dos.writeUTF(msg);
			} catch (IOException e) {
				System.out.println("------2------");
				this.release();
			}
		}
		
		// 释放资源
		private void release() {
			this.isRuning = false;
			IOUtils.close(dis, dos, socket);
		}
		
		@Override
		public void run() {
			while(isRuning) {
				String msg = this.receive();
				if (!msg.equals("")) {
					this.send(msg);
				}
			}
		}
	}
}

3、多线程封装发送端

package com.xzlf.chat;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream.GetField;
import java.net.Socket;

/**
 * 利用多线程封装发送端
 * 
 * @author xzlf
 *
 */
public class Send implements Runnable{
	private Socket socket;
	private DataOutputStream dos;
	private BufferedReader console;
	private boolean isRuning;

	public Send(Socket socket) {
		this.socket = socket;
		this.isRuning = true;
		try {
			console = new BufferedReader(new InputStreamReader(System.in));
			dos = new DataOutputStream(socket.getOutputStream());
		} catch (IOException e) {
			System.out.println("======1=====");
			this.release();
		}
	}
	
	// 从控制台获取消息
	private String getStrFromConsole() {
		try {
			return console.readLine();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return "";
	}
	
	// 发送消息
	public void send(String msg) {
		try {
			dos.writeUTF(msg);
			dos.flush();
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println("======3=====");
			this.release();
		}
	}
	
	// 释放资源
	private void release() {
		this.isRuning = false;
		IOUtils.close(dos, console, socket);
	}

	@Override
	public void run() {
		while(isRuning) {
			String msg = getStrFromConsole();
			if (!msg.equals("")) {
				this.send(msg);
			}
		}
		
	}

}

4、多线程封装接收端

package com.xzlf.chat;

import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;

/**
 * 使用多线程封装接收端
 * @author xzlf
 *
 */
public class Receive implements Runnable {
	
	private Socket socket;
	private DataInputStream dis;
	private boolean isRuning;
	
	public Receive(Socket socket) {
		this.socket = socket;
		this.isRuning = true;
		try {
			dis = new DataInputStream(socket.getInputStream());
		} catch (IOException e) {
			System.out.println("======2=====");
			this.release();
		}
	}
	// 接收消息
	public String receive() {
		String msg = "";
		try {
			msg = dis.readUTF();
		} catch (IOException e) {
			System.out.println(e);
			System.out.println("======4=====");
			release();
		}
		return msg;
	}

	// 释放资源
	private void release() {
		this.isRuning = false;
		IOUtils.close(dis, socket);
	}
	
	@Override
	public void run() {
		while(isRuning) {
			String msg = receive();
			if(!msg.equals("")) {
				System.out.println(msg);
			}
		}
		
	}
}

5、客户端

package com.xzlf.chat;

import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;

/**
 * 聊天室:客户端
 * @author xzlf
 *
 */
public class TMultiClient {
	public static void main(String[] args) throws UnknownHostException, IOException {
		System.out.println("======client======");
		// 1、指定ip + 端口 建立连接
		Socket socket = new Socket("localhost", 8888);
		// 2、客户端收发消息
		new Thread(new Send(socket)).start();
		new Thread(new Receive(socket)).start();
		
	}
}

运行服务端和客户端:
在这里插入图片描述
先每个客户端只能自己跟自己聊。

实现群聊:

1、加入容器(使用JUC包下的并发容器CopyOnWriteArrayList),并添加给其他用户发送消息方法
添加容器:

public class Chat {
	private static CopyOnWriteArrayList<Channel> all = new CopyOnWriteArrayList<Channel>();
	public static void main(String[] args) throws IOException {
		System.out.println("======server======");
		// 1、指定端口创建服务端
		ServerSocket server = new ServerSocket(8888);
		while(true) {
			// 2、每进来一个客户端启动一个线程
			Socket socket = server.accept();
			Channel c = new Channel(socket);
			all.add(c);
			System.out.println("一个客户端建立了连接");
			new Thread(c).start();
		}
	}
添加群发方法
// 群聊:发送消息给其他人
		private void sendOthers(String msg, boolean isSys) {
			for(Channel other : all) {
				if(other == this) {
					continue;
				}
				if (!isSys) {
					// 群聊消息
					other.send(this.name + "说:" + msg);
				}else {
					// 系统消息
					other.send(msg);
				}
			}
		}

2、在初始化发送端,写入自己用户名并在初始化就发送
客户端启动时,输入用户名

public class Client {
	public static void main(String[] args) throws UnknownHostException, IOException {
		System.out.println("======client======");
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		System.out.print("请输入用户名:");
		String name = br.readLine();
		// 1、指定ip + 端口 建立连接
		Socket socket = new Socket("localhost", 8888);
		// 2、客户端收发消息
		new Thread(new Send(socket, name)).start();
		new Thread(new Receive(socket)).start();
		
	}
}
发送线程初始化时立马发送用户名:
public class Send implements Runnable{
	private Socket socket;
	private DataOutputStream dos;
	private BufferedReader console;
	private boolean isRuning;
	private String name;
	public Send(Socket socket, String name) {
		this.socket = socket;
		this.name = name;
		try {
			this.isRuning = true;
			console = new BufferedReader(new InputStreamReader(System.in));
			dos = new DataOutputStream(socket.getOutputStream());
			// 发送用户名
			this.send(name);
		} catch (IOException e) {
			System.out.println("======1=====");
			this.release();
		}
	}
服务端的channel类中初始化时立马接收用户名并保存

3、服务端(静态内部类Channel类中)在初始化时立即获取用户名并给用户发送欢迎信息同时给其他用户发提示信息(系统消息)

static class Channel implements Runnable{
		private Socket socket;
		private DataInputStream dis;
		private DataOutputStream dos;
		private boolean isRuning;
		private String name;
		public Channel(Socket socket) {
			this.socket = socket;
			this.isRuning = true;
			try {
				dis = new DataInputStream(socket.getInputStream());
				dos = new DataOutputStream(socket.getOutputStream());
				// 获取用户名
				this.name = receive();
				this.send("欢迎你的到来");
				this.sendOthers(this.name + "来了xxx聊天室", true);
			} catch (IOException e) {
				System.out.println("------1------");
				this.release();
			}
			
		}

4、用户关闭线程,给其他用户发送提示信息,提示用户已离开

// 释放资源
		private void release() {
			this.isRuning = false;
			IOUtils.close(dis, dos, socket);
			// 退出
			all.remove(this);
			sendOthers(this.name + "离开了聊天室。。。", true);
		}
		

运行测试:
在这里插入图片描述

实现私聊:

通过判断用户输入信息是否包含“@xxx:”确定是否为私聊,修改群发方法:

/**
		 * 群聊:获取自己的信息,发送消息给其他人
		 * 私聊:约定数据格式: @xxx:msg
		 * @param msg
		 * @param isSys
		 */
		private void sendOthers(String msg, boolean isSys) {
			if(msg.startsWith("@")) {
				// 私聊
				int endIndex = msg.indexOf(":");
				String targetName = msg.substring(1, endIndex);
				String info = msg.substring(endIndex + 1);
				for(Channel other : all) {
					if(other.name.equals(targetName)) {
						other.send(this.name + "悄悄对你说:" + info);
					}
				}
			}else {
				// 群聊
				for(Channel other : all) {
					if(other == this) {
						continue;
					}
					if (!isSys) {
						// 群聊消息
						other.send(this.name + "说:" + msg);
					}else {
						// 系统消息
						other.send(msg);
					}
				}
			}
		}

好了,现在已经实现了私聊。
运行测试一下:
在这里插入图片描述

需要完整代码的可以在下方留言。

posted @ 2020-04-06 03:36  行者老夫  阅读(1479)  评论(2编辑  收藏  举报