Java小练手项目:用Java Socket实现多人聊天室,聊天室功能包括传输聊天内容或者文件。相比于其它的聊天室,增加了传输文件的功能供参考。
分成服务端和客户端两部分来写
服务端包括监听线程和处理收发信线程:
客户端包括发送消息线程和接收消息线程:
项目的目录结构如下所示
接下来,给出实际的代码进行分析
监听线程
- package Server;
-
- import java.io.IOException;
- import java.net.InetAddress;
- import java.net.ServerSocket;
- import java.net.Socket;
- import java.util.ArrayList;
- import java.util.List;
-
- public class MultiServer {
- public static void main(String[] args) {
- ServerSocket ss = null;
- Socket s = null;
-
- // 定义一个List列表来保存每个客户端,每新建一个客户端连接,就添加到List列表里。
- List
listSocket = new ArrayList<>(); - try {
- // 1. 创建ServerSocket类型的对象并提供端口号
- ss = new ServerSocket(9999);
- // 2. 等待客户端的连接请求,调用accept方法
- // 采用多线程的方式,允许多个用户请求连接。
- int i = 0;
- while (true) {
- System.out.println("等待客户端的连接请求...");
- s = ss.accept();
- listSocket.add(s);
- //sArr[i] = s;
- i++;
- System.out.printf("欢迎用户%d加入群聊!\n", i);
- System.out.printf("目前群聊中共有%d人\n", listSocket.size());
- InetAddress inetAddress = s.getInetAddress();
- System.out.println("客户端" + inetAddress + "连接成功!");
- // 调用多线程方法,每一个连上的客户端,服务器都有一个线程为之服务
- new MultiServerThread(s, inetAddress, listSocket).start();
- }
-
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- // 关闭流
- try {
- ss.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- try {
- s.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
-
- }
- }
-
- 复制代码
上述代码实现服务器监听客户端连接,利用accept
方法,每加入一个客户端,服务器都创建一个线程为之服务,同时将其加入一个List集合中,用来保存已加入聊天室的所有客户端。
处理收发信的线程
- package Server;
-
- import java.io.BufferedInputStream;
- import java.io.BufferedOutputStream;
- import java.io.IOException;
- import java.net.InetAddress;
- import java.net.Socket;
- import java.util.Arrays;
- import java.util.List;
-
- public class MultiServerThread extends Thread {
- private Socket s;
- private InetAddress inetAddress;
- private List
listSockets; -
- public MultiServerThread(Socket s, InetAddress inetAddress, List
listSockets) { - this.s = s;
- this.inetAddress = inetAddress;
- this.listSockets = listSockets;
- }
-
- public void BroadCast(Socket s, byte[] by, int res)
- {
- // 将服务器接收到的消息发送给除了发送方以外的其他客户端
- int i = 0;
- for (Socket socket: listSockets)
- {
- if (s!=socket) // 判断不是当前发送的客户端
- {
- System.out.println("发送给用户: " + listSockets.indexOf(socket));
- BufferedOutputStream ps = null;
- try {
- ps = new BufferedOutputStream(socket.getOutputStream());
- ps.write(by, 0, res); // 写入输出流,将内容发送给客户端的输入流
- ps.flush();
-
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
-
- // 服务器与客户端的交互线程
- @Override
- public void run() {
- BufferedInputStream ois = null;
- BufferedOutputStream oos = null;
- try {
- ois = new BufferedInputStream(s.getInputStream());
-
- oos = new BufferedOutputStream(s.getOutputStream());
-
- int i = 0;
- while (true) {
- //System.out.println("进入MultiChatServerThread");
- byte[] by = new byte[1024+2];
- //System.out.println("by.length: " + by.length);
- int res = 0;
- res = ois.read(by);
- // 对读取到的字节数组第一位位置进行修改,标识该数据流是由哪个用户发送来的
- by[0] = (byte)listSockets.indexOf(s);
-
- if (by[1] == 2){
- // 因为前两个位置是标志位,所以length的大小为读取的字节数-2,同时offset也从第三个位置(下标是2)开始读
- String receive = new String(by, 2, res-2);
-
- if (receive.equalsIgnoreCase("bye"))
- {
- // 如果客户端发送的是bye, 说明其下线,则从listSockets里删除对应的socket.
- oos.write(receive.getBytes()); // 把bye给客户端的读取线程,从而可以关闭掉读取线程
- oos.flush();
- System.out.printf("用户%d下线, ", listSockets.indexOf(s));
- listSockets.remove(s);
- System.out.printf("目前聊天室仍有%d人\n", listSockets.size());
- }
- }
- System.out.println("i" + i + "res = " + res);
- System.out.println("by.length: " + by.length);
- System.out.println("Socket[]: " + Arrays.toString(listSockets.toArray()));
- // 调用函数,将接受到的消息发送给所有客户端
- BroadCast(s, by, res);
-
- }
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- try {
- if (oos != null) {
- oos.close();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- try {
- if (ois != null) {
- ois.close();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- }
- }
-
- 复制代码
在处理收发信的线程中,利用BroadCast()
方法将服务器接收到的消息发送给除了发送方以外的其他客户端。
在run()
方法中,创建字节数组来接收客户端发送的数据流。定义字节数组时,byte[] by = new byte[1024+2];
这里+2的原因是,为了区分发送的用户以及传输的数据类型是消息文本还是文件。其中,第一位标志位用来表示用户的id,第二位标志位用1,2来表示发送的是消息还是文件,1表示发送的是消息,2表示发送的是文件。 下面客户端代码时可以更好理解。
判断用户下线的标志是用户发送bye
,说明其下线,则服务端从listSockets里删除对应的socket。同时,将把bye
发送给客户端的读取线程,提示其可以关闭掉读取线程。
以上就是服务端的实现逻辑。整体思路就是:
客户端的两个线程包括发送消息给服务器和读取服务器发送的消息,用主线程和子线程来分别实现。
客户端1
- package Client;
-
- import java.io.*;
- import java.net.Socket;
- import java.text.SimpleDateFormat;
- import java.util.Date;
- import java.util.Scanner;
-
- public class MultiClient extends Thread {
- private Socket ss;
-
- public MultiClient() {
- }
-
- public MultiClient(Socket ss) {
- this.ss = ss;
- }
-
- public byte[] reviseArr(byte[] by, int res) {
- byte[] newByteArr = new byte[by.length + 2];
- // 将by字节数组的内容都往后移动两位,即头部的两个位置空出来作为标志位
- for (int i = 0; i < by.length; i++)
- {
- newByteArr[i+2] = by[i];
- }
- return newByteArr;
- }
-
- // 子线程执行读操作,读取服务端发回的数据
- @Override
- public void run() {
- BufferedInputStream bis = null;
- BufferedOutputStream bosFile = null; // 与输出文件流相关联
- try {
- bis = new BufferedInputStream(ss.getInputStream());
- //bosFile = new BufferedOutputStream(new FileOutputStream("./directoryTest/src/用户1 IO流的框架图.png"));
- // 等待接收服务器发送回来的消息
- while(true) {
- byte[] by = new byte[1024+2];
- int res = bis.read(by);
- int sendUser = by[0];
- Date date = new Date();
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
- String format = sdf.format(date);
- if (by[1] == 1) // 说明传的是文件
- {
- //String filePath = String.format("./directoryTest/src/用户%d传送来的IO流的框架图.png", sendUser);
- bosFile = new BufferedOutputStream(new FileOutputStream("./directoryTest/用户" + sendUser + "-传输的文件.png", true));
- bosFile.write(by, 2, res-2);
- bosFile.flush();
- if (res<1026) // 说明是最后一次在传送文件,所以传送的字节数才会小于字节数组by的大小
- {
- System.out.println("用户" + sendUser + "\t" + format + ":");
- System.out.printf("用户%d发送的文件传输完成\n", sendUser);
- }
- }
- else // 说明传输的是聊天内容,则按字符串的形式进行解析
- {
- // 利用String构造方法的形式,将字节数组转化成字符串打印出来
- String receive = new String(by, 2, res);
- System.out.println("用户" + sendUser + "\t" + format + ":");
- System.out.println(receive);
- }
- }
- } catch (IOException e) {
- e.printStackTrace();
- }finally{
- try {
- bis.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
-
- public static void main(String[] args) {
- // 主线程执行写操作,发送消息到服务器
- Socket ss = null;
- BufferedOutputStream bos = null;
- BufferedInputStream bis = null; // 与文件关联的流
- MultiClient mcc = null;
- try {
- ss = new Socket("127.0.0.1", 9999);
- System.out.println("服务器连接成功");
- System.out.println("-----------聊天室-----------");
- bos = new BufferedOutputStream(ss.getOutputStream());
- Scanner sc = new Scanner(System.in);
- mcc = new MultiClient(ss);
- mcc.start();
- byte[] by = new byte[1024];
- int res = 0;
- int i = 0;
- while(true) {
- // 由用户输入选择执行不同的传输任务
- // 若用户输入传输文件,则传输指定文件,否则,则正常聊天任务
- String str = sc.nextLine();
- if (str.equals("传输文件")) {
- bis = new BufferedInputStream(new FileInputStream("./directoryTest/壁纸1.png"));
-
- while ((res = bis.read(by)) != -1) {
- //System.out.println("i" + i + " res: " + res);
- byte[] newByteArr = mcc.reviseArr(by, res);;
- newByteArr[1] = 1; // 表示第二个位置上的值为1时表示传输的是文件
- bos.write(newByteArr, 0, res+2);
- bos.flush();
-
- }
- }
- else{
- byte[] sb = str.getBytes(); // 转化为字节数组
- byte[] newByteArr = mcc.reviseArr(sb, sb.length);
- newByteArr[1] = 2; // 表示第二个位置上的值为2时表示传输的是聊天内容
- bos.write(newByteArr); // 把内容发给服务器
- bos.flush();
- if (str.equalsIgnoreCase("bye"))
- {
- System.out.println("用户下线!");
- break;
- }
- }
-
- }
- }catch (IOException e) {
- e.printStackTrace();
- }finally {
- try {
- bos.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- try {
- mcc.stop();
- ss.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
-
- 复制代码
在客户端实现中,主线程执行写操作,发送消息到服务器,接收键盘的标准输入。在主线程发送消息时,判断用户的输入,如果输入的文本内容是传输文件,则会去读取指定路径下的文件,并利用BufferedOutputStream
方法将文件转化为字节缓冲输出流发送。
这里reviseArr
方法,是将读入文件输入流的102大小的字节数组往后移动两位,实现前两位作为标志位,区分用户id和传输数据类型的目的。如果传输的是文件,会在第二个标志位赋1,如果传输的是消息文本,则第二个标志位赋2。
若用户发送的消息是"bye",则表示用户下线,利用break
跳出主线程循环,并在finally
中调用mcc.stop()
关闭子线程,从而关闭该客户端。
在子线程中,若用户发送的是文件,则利用字节缓冲输入流BufferedInputStream
将文件写入到指定路径中,并在文件名中简易标识发送用户。若接收的字节数组长度小于设定的1026,说明是最后一次在传送文件,在将最后一次文件的输入流写入后,在控制台打印文件传输完成的提示信息。
以上是客户端1的实现代码,其它客户端的实现代码类似
客户端2
- package Client;
-
- import java.io.*;
- import java.net.Socket;
- import java.text.SimpleDateFormat;
- import java.util.Date;
- import java.util.Scanner;
-
- public class MultiClient2 extends Thread {
- private Socket ss;
-
- public MultiClient2() {
- }
-
- public MultiClient2(Socket ss) {
- this.ss = ss;
- }
-
- public byte[] reviseArr(byte[] by, int res) {
- byte[] newByteArr = new byte[by.length + 2];
- // 将by字节数组的内容都往后移动两位,即头部的两个位置空出来作为标志位
- for (int i = 0; i < by.length; i++)
- {
- newByteArr[i+2] = by[i];
- }
- return newByteArr;
- }
-
- @Override
- public void run() {
- BufferedInputStream bis = null;
- BufferedOutputStream bosFile = null; // 与输出文件流相关联
- try {
- bis = new BufferedInputStream(ss.getInputStream());
-
- // 等待接收服务器发送回来的消息
-
- while(true) {
- byte[] by = new byte[1024+2];
- int res = bis.read(by);
- int sendUser = by[0];
- Date date = new Date();
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
- String format = sdf.format(date);
- if (by[1] == 1) // 说明传的是文件
- {
- //String filePath = String.format("./directoryTest/src/用户%d传送来的IO流的框架图.png", sendUser);
- bosFile = new BufferedOutputStream(new FileOutputStream("./directoryTest/用户" + sendUser + "-传输的文件.png", true));
- bosFile.write(by, 2, res-2);
- bosFile.flush();
- if (res<1026) // 说明是最后一次在传送文件,所以传送的字节数才会小于字节数组by的大小
- {
- System.out.println("用户" + sendUser + "\t" + format + ":");
- System.out.printf("用户%d发送的文件传输完成\n", sendUser);
- }
- }
- else // 说明传输的是聊天内容,则按字符串的形式进行解析
- {
- // 利用String构造方法的形式,将字节数组转化成字符串打印出来
- String receive = new String(by,2, res);
- System.out.println("用户" + sendUser + "\t" + format + ":");
- System.out.println(receive);
- }
-
- }
- } catch (IOException e) {
- e.printStackTrace();
- }finally{
- try {
- bis.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
-
- public static void main(String[] args) {
- // 主线程写操作
-
- Socket ss = null;
- BufferedOutputStream bos = null;
- BufferedInputStream bis = null; // 与文件关联的流
- MultiClient2 mcc = null;
- try {
- ss = new Socket("127.0.0.1", 9999);
- System.out.println("服务器连接成功");
- System.out.println("-----------聊天室-----------");
- bos = new BufferedOutputStream(ss.getOutputStream());
- Scanner sc = new Scanner(System.in);
- mcc = new MultiClient2(ss);
- mcc.start();
- byte[] by = new byte[1024];
- int res = 0;
- int i = 0;
- while(true) {
- String str = sc.nextLine();
- if (str.equals("传输文件")) {
- bis = new BufferedInputStream(new FileInputStream("./directoryTest/壁纸1.png"));
- while ((res = bis.read(by)) != -1) {
- i += 1;
- //System.out.println("i" + i + " res: " + res);
- byte[] newByteArr = mcc.reviseArr(by, res);;
- newByteArr[1] = 1; // 表示第二个位置上的值为1时表示传输的是文件
- bos.write(newByteArr, 0, res+2);
- bos.flush();
-
- }
- }
- else{
- byte[] sb = str.getBytes(); // 转化为字节数组
- byte[] newByteArr = mcc.reviseArr(sb, sb.length);
- //System.out.println("newByteArr: " + Arrays.toString(newByteArr));
- newByteArr[1] = 2; // 表示第二个位置上的值为2时表示传输的是聊天内容
- bos.write(newByteArr); // 把内容发给服务器
- bos.flush();
- if (str.equalsIgnoreCase("bye"))
- {
- System.out.println("用户下线!");
- break;
- }
- }
-
- }
- }catch (IOException e) {
- e.printStackTrace();
- }finally {
- try {
- bos.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- try {
- mcc.stop();
- ss.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
-
- 复制代码
客户端3
- package Client;
-
- import java.io.*;
- import java.net.Socket;
- import java.text.SimpleDateFormat;
- import java.util.Date;
- import java.util.Scanner;
-
- public class MultiClient3 extends Thread {
- private Socket ss;
-
- public MultiClient3() {
- }
-
- public MultiClient3(Socket ss) {
- this.ss = ss;
- }
-
- public byte[] reviseArr(byte[] by, int res) {
- byte[] newByteArr = new byte[by.length + 2];
- // 将by字节数组的内容都往后移动两位,即头部的两个位置空出来作为标志位
- for (int i = 0; i < by.length; i++)
- {
- newByteArr[i+2] = by[i];
- }
- return newByteArr;
- }
-
- @Override
- public void run() {
- BufferedInputStream bis = null;
- BufferedOutputStream bosFile = null; // 与输出文件流相关联
- try {
- bis = new BufferedInputStream(ss.getInputStream());
- // 等待接收服务器发送回来的消息
- while(true) {
- byte[] by = new byte[1024+2];
- int res = bis.read(by);
- int sendUser = by[0];
- Date date = new Date();
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
- String format = sdf.format(date);
- if (by[1] == 1) // 说明传的是文件
- {
- bosFile = new BufferedOutputStream(new FileOutputStream("./directoryTest/用户" + sendUser + "-传输的文件.png", true));
- bosFile.write(by, 2, res-2);
- bosFile.flush();
- if (res<1026) // 说明是最后一次在传送文件,所以传送的字节数才会小于字节数组by的大小
- {
- //System.out.println("客户端接收到的信息" + receive);
- System.out.println("用户" + sendUser + "\t" + format + ":");
- System.out.printf("用户%d发送的文件传输完成\n", sendUser);
- }
- }
- else // 说明传输的是聊天内容,则按字符串的形式进行解析
- {
- // 利用String构造方法的形式,将字节数组转化成字符串打印出来
- String receive = new String(by, 2, res);
- System.out.println("用户" + sendUser + "\t" + format + ":");
- System.out.println(receive);
- }
- }
- } catch (IOException e) {
- e.printStackTrace();
- }finally{
- try {
- bis.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
-
- public static void main(String[] args) {
- // 主线程写操作
- //MultiClient mc = new MultiClient();
- Socket ss = null;
- BufferedOutputStream bos = null;
- BufferedInputStream bis = null; // 与文件关联的流
- MultiClient3 mcc = null;
- try {
- ss = new Socket("127.0.0.1", 9999);
- System.out.println("服务器连接成功");
- System.out.println("-----------聊天室-----------");
- bos = new BufferedOutputStream(ss.getOutputStream());
- Scanner sc = new Scanner(System.in);
- mcc = new MultiClient3(ss);
- mcc.start();
- byte[] by = new byte[1024];
- int res = 0;
- int i = 0;
- while(true) {
- String str = sc.nextLine();
- if (str.equals("传输文件")) {
- bis = new BufferedInputStream(new FileInputStream("./directoryTest/壁纸1.png"));
- while ((res = bis.read(by)) != -1) {
- byte[] newByteArr = mcc.reviseArr(by, res);;
- newByteArr[1] = 1; // 表示第二个位置上的值为1时表示传输的是文件
- bos.write(newByteArr, 0, res+2);
- bos.flush();
-
- }
- }
- else{
- byte[] sb = str.getBytes(); // 转化为字节数组
- byte[] newByteArr = mcc.reviseArr(sb, sb.length);
- newByteArr[1] = 2; // 表示第二个位置上的值为2时表示传输的是聊天内容
- bos.write(newByteArr); // 把内容发给服务器
- bos.flush();
- // 如果用户输入bye则表示用户下线
- if (str.equalsIgnoreCase("bye"))
- {
- System.out.println("用户下线!");
- break;
- }
- }
-
- }
- }catch (IOException e) {
- e.printStackTrace();
- }finally {
- try {
- bos.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- try {
- mcc.stop();
- ss.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
-
- 复制代码
开启服务器和3个客户端,初始状态
聊天室
每有一个客户端连接,会打印相应的提示信息
三个客户端连接成功,打印提示信息
接下来,进行聊天功能的展示
可以看到,聊天室里标识出每个用户以及发送的时间和消息,可以实现基本的聊天功能。
传输文件
在客户端1输入传输文件
进入到写入的目录下,存在相应的文件:
即实现了聊天室和传输文件的功能,最后客户端1发送bye
,该客户端断开连接