概述
使用TCP的Socket实现一个聊天室
服务器端:一个线程专门发送消息,一个线程专门接受消息。
客户端:一个线程专门发送消息,一个线程专门接受消息。
- 1、群聊:
重点在于创建一个容器,搭建一个消息转发器,最终实现群聊
的功能。 - 2、私聊:
模拟报文,分析数据,转发给特定的某个人。 - 3、注意:
没有请求和响应的模式,每个客户端发送和接收数据各有一个
线程去实现,客户间彼此发言不受干扰。
一、群聊
服务器端代码:
public class Chat {
//在每个客户端启动线程之前,丢入容器当中
//如果多线程中,对容器又要改又要遍历,不建议使用ArrayList
//推荐使用CopyOnWriteArrayList
//它会自动复制一份容器信息,在发生修改容器的情况时,将拷贝的信息同步更新到容器中,防止并发出错
private static CopyOnWriteArrayList<Channel> all = new CopyOnWriteArrayList<Channel>();
@SuppressWarnings("resource")
//主方法体
public static void main(String[] args) throws IOException {
System.out.println("--------Server--------");
//1、指定端口,使用ServerSocket创建服务器
ServerSocket server = new ServerSocket(9999);
//2、阻塞式等待连接 accept
while(true){
Socket client = server.accept();
System.out.println("一个客户端建立了连接");
Channel channel = new Channel(client);
//先添加进容器,交给容器管理后,再启动多线程
all.add(channel);
new Thread(channel).start();
}
}
//定义静态内部类:一个客户代表一个Channel
static class Channel implements Runnable{
private DataInputStream dis;
private DataOutputStream dos;
private Socket client;
private boolean isRunning;
private String id;
public Channel(Socket client){
this.client = client;
try {
dis = new DataInputStream(client.getInputStream());
dos = new DataOutputStream(client.getOutputStream());
isRunning = true;
//获取ID
this.id = receive();
this.send("欢迎加入群聊,您可以畅所欲言啦~");
sendOthers(this.id+"已加入群聊",true);
} catch (Exception e1) {
System.out.println("******传输流出错******");
release();
}
}
//接收消息
private String receive(){
String msg = "";
try {
msg = dis.readUTF();
} catch (IOException e) {
System.out.println("******接收消息出错******");
release();
}
return msg;
}
//发送消息
private void send(String msg){
try {
dos.writeUTF(msg);
dos.flush();
} catch (IOException e) {
System.out.println("******发送消息出错******");
release();
}
}
//群聊:获取自己的消息,发送给其他人
private void sendOthers(String msg,boolean isSystem){
for(Channel other:all){ //遍历成员
if(other==this){ //判断是否是自己
continue;
}
if(!isSystem){ //判断是否是系统消息
other.send(this.id+":"+msg); //发给其他人
}else{
other.send("公告:"+msg); //系统发送给其他人
}
}
}
//释放资源
private void release(){
this.isRunning = false;
Utils.close(dos,dis,client);
//退出:从容器中除去此用户
all.remove(this);
sendOthers(this.id+"已退出群聊",true);
}
//重写run方法
public void run() {
while(isRunning){
String msg = receive();
if(msg!=null) sendOthers(msg,false);
}
}
}
}
客户端代码
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.println("请输入用户名");
String id = br.readLine();
//1、建立连接:使用Socket创建客户器+服务器的地址和端口
Socket client = new Socket("localhost",9999);
//2、客户端发送消息
new Thread(new Send(client,id)).start();
new Thread(new Receive(client)).start();
}
}
自定义接收类
使用多线程封装接收端
- 1、接收消息
- 3、重写run()
- 4、释放资源
public class Receive implements Runnable{
private DataInputStream dis;
private Socket client;
private boolean isRunning;
public Receive(Socket client){
this.client = client;
this.isRunning = true;
try {
dis = new DataInputStream(client.getInputStream());
} catch (IOException e) {
System.out.println("******接收消息出错******");
release();
}
}
public void run() {
while(isRunning){
String msg = receive();
if(msg!=null&&(!msg.equals(""))){
System.out.println(msg);
}
}
}
//接收消息
private String receive(){
String msg = "";
try {
msg = dis.readUTF();
} catch (IOException e) {
System.out.println("******接收消息出错******");
release();
}
return msg;
}
//释放资源
private void release(){
this.isRunning = false;
Utils.close(dis,client);
}
}
自定义发送类
使用多线程封装发送端
- 1、发送消息
- 2、从控制台获取消息
- 3、重写run()
- 4、释放资源
public class Send implements Runnable{
private BufferedReader br;
private DataOutputStream dos;
private Socket client;
private boolean isRunning;
private String id;
//用户ID直接添加进构造器,在流对接前就准备好
public Send(Socket client,String id){
this.id = id;
this.client = client;
this.isRunning = true;
br = new BufferedReader(new InputStreamReader(System.in));
try {
dos = new DataOutputStream(client.getOutputStream());
//发送ID
send(id);
} catch (IOException e) {
System.out.println("******传输流出错******");
release();
}
}
//重写run方法
public void run() {
while(isRunning){
String msg = getStr();
if(msg!=null&&(!msg.equals(""))){
send(msg);
}
}
}
//发送消息
private void send(String msg){
try {
dos.writeUTF(msg);
dos.flush();
} catch (IOException e) {
System.out.println("******发送消息出错******");
release();
}
}
//从控制台获得消息
private String getStr(){
try {
return br.readLine();
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
//释放资源
private void release(){
this.isRunning = false;
Utils.close(dos,client);
}
}
自定义工具类
1、目的:释放资源
2、Closeable:Socket都实现了Closeable,可以用可变参数简化代码,提高可
维护性。用法类似于数组。
3、注意:只要释放资源,都要注意加空判断。
public class Utils {
//释放资源
public static void close(Closeable... targets){ //可变参数
//用法类似于数组
for(Closeable target:targets){
try{
if(target!=null)
target.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
}
二、添加私聊功能
只需要对信息进行分析。
约定数据格式: @xxx:msg (这是我们自定义的协议)
故只需将 sendOthers 方法改成下面的代码:
private void sendOthers(String msg,boolean isSystem){
boolean isPrivate = msg.startsWith("@");
//判断是私聊还是群聊
if(isPrivate){ //私聊
int idx = msg.indexOf(":");
//获取目标ID和消息
String targetName = msg.substring(1,idx);
msg = msg.substring(idx+1);
//遍历
for(Channel other:all){
if(other.id.equals(targetName)){ //找到目标ID
other.send(this.id+"悄悄对你说:"+msg); //发给其他人
break; //找到目标对象,并发完悄悄话后,便退出遍历
}
}
}else{ //群聊
for(Channel other:all){ //遍历成员
if(other==this){ //判断是否是自己
continue;
}
if(!isSystem){ //判断是否是系统消息
other.send(this.id+":"+msg); //发给其他人
}else{
other.send("公告:"+msg); //系统发送给其他人
}
}
}
}