Java小练手项目:用Java Socket实现多人聊天室,聊天室功能包括传输聊天内容或者文件。相比于其它的聊天室,增加了传输文件的功能供参考。
模块拆解
分成服务端和客户端两部分来写。
服务端包括监听线程和处理收发信线程:
- 创建监听线程,监听客户端的连接。将每个连接的客户端加入维护的列表,并为每个连接的客户端开启一个处理收发信的线程。
- 在每个客户端的收发信线程中,接收每个客户端发回的消息,并对其进行转发到相应接收的客户端上,以此实现多人聊天室。
- 添加处理传输文件的判断,通过在传输的字节数组中添加标志位来区分传输的是文本消息,还是文件。
客户端包括发送消息线程和接收消息线程:
- 发送消息线程,用来处理用户的输入信息,判断输入的是文本信息还是文件,并修改传输的字节数组标志位进行区分。最后将信息传输给服务器。
- 接收消息线程,用来处理服务器发回的信息,根据标志位判断输入的是文本信息还是文件,并做相应处理。如果是文本信息,则显示在控制台,如果是文件,则保存在指定目录下。
项目的目录结构如下所示
接下来,给出实际的代码进行分析。
服务器端
监听线程
<pre class="prettyprint hljs gradle" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">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<Socket> 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集合中,用来保存已加入聊天室的所有客户端。
处理收发信的线程
<pre class="prettyprint hljs cs" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">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<Socket> listSockets;
public MultiServerThread(Socket s, InetAddress inetAddress, List<Socket> 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 发送给客户端的读取线程,提示其可以关闭掉读取线程。
以上就是服务端的实现逻辑。整体思路就是:
- 首先创建监听线程,接收每个客户端的连接请求,并创建一个List集合保存。
- 创建一个处理收发信的线程,即每个客户端发送的聊天内容,都先统一发回给服务器端,再由服务器端进行集中转发给每个客户端。
客户端的两个线程包括发送消息给服务器和读取服务器发送的消息,用主线程和子线程来分别实现。
客户端1
<pre class="prettyprint hljs cs" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">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
<pre class="prettyprint hljs cs" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">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
<pre class="prettyprint hljs cs" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">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 ,该客户端断开连接
<pre class="hljs" style="padding: 0.5em; font-family: Menlo, Monaco,
作者:Java熬夜党
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。