学习网络与通信,实现聊天界面能够通过服务器进行私聊和群聊的功能。
1.服务器:ServeSocket
客户端先发送消息给服务器,服务器接受消息后再发送给客户端。
利用服务器随时监听。等待客户端的请求,一旦有请求便生产一个socket套接字用于双方之间的数据传输。在此需要指定服务器的端口号。
端口号:区分一个IP地址中套接字。
package ChatV2.Server;
import java.io.IOException;
import java.net.ServerSocket;
public class Server {
private int port = 50001;
ServerSocket serverSocket;
{
try {
serverSocket = new ServerSocket(port);
System.out.println("创建服务端成功");
ListenerRunnable lr = new ListenerRunnable(serverSocket);
new Thread(lr).start();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
new Server();
}
}
同时在服务器中我还创建了两个线程,一个用于不断接受新的客户端的连接,一个用于与各客户端的发送接受消息。
1.1接受客户端连接的线程
这个线程需要接受用户端的连接,那么就需要创建的serverSocket对象,利用ServerSocket.accept();方法接受对象,后接受其创建后返回的socket对象。此方法为阻塞方法,一直等待客户端的接入。
并且将此对象放入创建的Socket套接字的队列中,以便使用。
package ChatV2.Server;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ArrayBlockingQueue;
public class ListenerRunnable implements Runnable{
private ServerSocket serverSocket;
private Socket socket;
private OutputStream os;
//存放接入的socket
private static ArrayBlockingQueue<Socket> sockets = new ArrayBlockingQueue<>(10);
public ListenerRunnable(ServerSocket serverSocket) {
this.serverSocket = serverSocket;
}
public ListenerRunnable() {
}
@Override
public void run() {
while (true) {
try {
socket = serverSocket.accept();//接受,阻塞方法,
//存入到sockets中
sockets.offer(socket);
System.out.println(socket.getPort() + "连接成功");
//savePort();
InputStream is = socket.getInputStream();
//创建接受信息的线程
InformationRunnable ir = new InformationRunnable(is,socket,sockets);
//发送给其他所有用户 当前用户的端口号
String socketMsg = "3#";
for (int i = 0; i < sockets.size(); i++) {
Socket temSocket = sockets.poll();
System.out.println("发送端口号");
socketMsg += temSocket.getPort() + "#";
sockets.offer(temSocket);
}
groupOutput(socketMsg);
new Thread(ir).start();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
//群发所有端口
public void groupOutput(String msg) {
try {
int size = sockets.size();
for (int i = 0; i < size; i++) {
System.out.println("多少个用户端:" + sockets.size());
Socket temSocket = sockets.poll();
os = temSocket.getOutputStream();
output(msg);
sockets.offer(temSocket);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void output(String msg) {
try {
byte[] msgBytes = msg.getBytes(StandardCharsets.UTF_8);//转化为字节型
int ml = msgBytes.length;
os.write(ml);//大小
for (int i = 0; i < msgBytes.length; i++) {
os.write(msgBytes[i]);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
同时在此接受到新客户端的连接后,将此时所有的端口号再转发给所有的客户端用于私聊功能的实现。
输出流的写入实现方法:
利用套接字中OutputStream 与 InputStream 来读取与写入字符
每次写入与读取只能写入/读取一个字节,那么就会存在多个字节如何组成我们想要的字符,以及接受端究竟该接受多少个字符才能写入段的一句完成的对话的问题。
第一个问题的解决:
只需使用相同的字符编码方式,将字符串类型转化为字节类型中选择UTF-8的字符编码方式,接受端也是用此编码方式 即可保证为相同的字符。
第二个问题的解决:
我们将字符串转化为字节类型,用数组存储。先将此数组的大小传过去,之后接收端创建相同大小的数组用于接受,接受完后转为字符串即可。
int ml = is.read();//用于接受字节大小
byte[] msgBytes = new byte[ml];//发过来的一串信息
for (int i = 0; i < msgBytes.length; i++) {
int readByte = is.read();
msgBytes[i] = (byte) readByte;
}
String msg = new String(msgBytes, StandardCharsets.UTF_8);
1.2接受消息与发送消息的线程
这个线程需要判断发过来的信息是群发还是私聊。我的发送过来的消息的格式为“ 字符1# 字符2”如果字符21为0则为群聊,字符二为发送的消息。“字符一#字符二#字符三”如果字符1 为1则为私聊,此时字符二为私聊的端口号,字符三为消息。用‘#’进行分割。
package ChatV2.Server;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ArrayBlockingQueue;
public class InformationRunnable implements Runnable {
private InputStream is;
private OutputStream os;
private Socket socket;
private ArrayBlockingQueue<Socket> sockets;
public InformationRunnable(InputStream is, Socket socket, ArrayBlockingQueue<Socket> sockets) {
this.is = is;
this.socket = socket;
this.sockets = sockets;
}
@Override
public void run() {
while (true) {
getIMG();
}
}
//接受消息
public void getIMG() {
try {
System.out.println("客户端说:");
int ml = is.read();//用于接受字节大小
byte[] msgBytes = new byte[ml];//发过来的一串信息
for (int i = 0; i < msgBytes.length; i++) {
int readByte = is.read();
msgBytes[i] = (byte) readByte;
}
String msg = new String(msgBytes, StandardCharsets.UTF_8);
String[] msg1 = msg.split("#");//分开
if (msg1[0].equals("0")) {//群聊
System.out.println("群聊消息:");
groupOutput(msg1[1] + "#");
} else {//私聊 确认发送对象
System.out.println("私聊消息:");
String msg0 = "";//发送给发送信息的人
int size = sockets.size();
for (int i = 0; i < size; i++) {
System.out.println("查找私聊对象");
Socket temSocket = sockets.poll();
if (msg1[1].equals(String.valueOf(temSocket.getPort()))) {//给私聊对象发送
os = temSocket.getOutputStream();
msg1[2] = "私聊:" + msg1[2] +"#";
msg0 = "私聊给:" + msg1[1] + msg1[2] +"#";
output(msg1[2]);
}
sockets.offer(temSocket);
}
//发送给消息对象
os = socket.getOutputStream();
output(msg0);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//群发方式
public void groupOutput(String msg) {
try {
int size = sockets.size();
for (int i = 0; i < size; i++) {
System.out.println("多少个用户端:" + sockets.size());
Socket temSocket = sockets.poll();
os = temSocket.getOutputStream();
output(msg);
sockets.offer(temSocket);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//发送消息
public void output(String msg) {
try {
byte[] msgBytes = msg.getBytes(StandardCharsets.UTF_8);//转化为字节型
int ml = msgBytes.length;
os.write(ml);//大小
for (int i = 0; i < msgBytes.length; i++) {
os.write(msgBytes[i]);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//以端点命名的文件,将端点及聊天内容全部写入文件过来。
public synchronized void saveInformation(String information) throws IOException {
String path = "C:\\Users\\15697\\IdeaProjects\\Pro24\\src\\Chat\\UsersInformation\\document";
FileWriter fw = new FileWriter(path, true);
BufferedWriter bw = new BufferedWriter(fw);
bw.write(information);
bw.write("#");//分割消息
System.out.println("写入消息:" + information);
bw.close();
}
}
将字节数组转化为字符串后,里面split("#");方法将字符串分割为多个字符串数组msg1。判断msg1[0]是否为0来区分群聊或者私聊。
1.21群聊的实现
判断后调用groupOutput()群聊方法。
在这方法会将我们先前存入的socket队列一 一取出并 使用它的outputStream发送消息
public void groupOutput(String msg) {
try {
int size = sockets.size();
for (int i = 0; i < size; i++) {
System.out.println("多少个用户端:" + sockets.size());
Socket temSocket = sockets.poll();
os = temSocket.getOutputStream();
output(msg);
sockets.offer(temSocket);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//发送消息
public void output(String msg) {
try {
byte[] msgBytes = msg.getBytes(StandardCharsets.UTF_8);//转化为字节型
int ml = msgBytes.length;
os.write(ml);//大小
for (int i = 0; i < msgBytes.length; i++) {
os.write(msgBytes[i]);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
1.22私聊的实现
与群聊不同的是他需要判断私聊的对象。将各个套接字取出后对比msg1[1]与各个套接字的端口号。找到了则取出它的输出流,设置发送的字符,调用output()方法即可。同时需要发送给消息的发送者,让其显示在界面上。
System.out.println("私聊消息:");
String msg0 = "";//发送给发送信息的人
int size = sockets.size();
for (int i = 0; i < size; i++) {
System.out.println("查找私聊对象");
Socket temSocket = sockets.poll();
if (msg1[1].equals(String.valueOf(temSocket.getPort()))) {//给私聊对象发送
os = temSocket.getOutputStream();
msg1[2] = "私聊:" + msg1[2] +"#";
msg0 = "私聊给:" + msg1[1] + msg1[2] +"#";
output(msg1[2]);
}
sockets.offer(temSocket);
}
//发送给消息对象
os = socket.getOutputStream();
output(msg0);
到此服务器的接受与发送功能以实现。
2.客户端:
需要知道服务器的ip以及端口号,设置即可
package ChatV2.Client;
import ChatV2.DATA;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class Clint implements DATA {
private Socket socket;//IP 端点
public int getPort() {
return socket.getLocalPort();
}
private OutputStream os;
private InputStream is;
public InputStream getInputStream() {
return is;
}
{
try {
socket = new Socket("127.0.0.1", 50001);
os = socket.getOutputStream();
is = socket.getInputStream();
} catch (IOException e) {
/* throw new RuntimeException(e);*/
System.out.println("无法连接服务器");
}
}
public void output(String msg, int index) {
try {
//使用#隔开 第一个为类型,第二个为端口号,第三部分为内容 0为私聊
String s = "";
if (IMG[1] == 0) {
s += IMG[1] + "#" + msg;
} else {
s += IMG[1] + "#" + PORT.get(index) + "#" + msg;
}
System.out.println("待发送消息:" + s);
byte[] msgBytes = s.getBytes(StandardCharsets.UTF_8);
int ml = msgBytes.length;
os.write(ml);
for (int i = 0; i < ml; i++) {
os.write(msgBytes[i]);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
同时我在里面写了一个发送给服务器信息的方法output(String msg,int index)msg为发送的信息,格式与服务器接受的格式一致,index则为接受服务端发送端口号后,存入的信息数组的下标,以便确认私聊对象。
其他的交给界面UI来实现。
3.聊天界面:
结构:以及在此调用用户端。
DATA接口中创建了存放界面需要多次使用的数据。用户的端口号,以及生成的单选按钮,选择聊天对象,以及数据。
import javax.swing.*;
import java.util.ArrayList;
public interface DATA {
//存放用户
ArrayList<String> PORT = new ArrayList<>();
//用来存放按钮,表示各用户
JRadioButton[] JRB = new JRadioButton[10];
//下标0为记录连接人数以及群聊。1为聊天方式为群聊或私聊
int[] IMG = new int[2];
}
界面的设置不多说:
package ChatV2;
import ChatV2.Client.Clint;
import javax.swing.*;
import java.awt.*;
public class ChatUI extends JFrame implements DATA{
Clint clint;
public ChatUI() {
setTitle("MyChat");
setSize(900, 700);
setLayout(null);
setDefaultCloseOperation(EXIT_ON_CLOSE);
//按钮组
ButtonGroup bg = new ButtonGroup();
//setLayout(new FlowLayout());
JLabel jl = new JLabel("群聊");
//输入框
JTextArea jta = new JTextArea();
jta.setBounds(10, 500, 700, 200);
//JScrollPane scrollPane = new JScrollPane(jta);//翻页的作用
//scrollPane.set
JButton jb = new JButton("发送");//需添加监听
jb.setBounds(710, 550, 75, 50);
ButtonListener bl = new ButtonListener(jta);
jb.addActionListener(bl);
//显示框:
JTextArea chatArea = new JTextArea( );//几行几列
// chatArea.setPreferredSize(new Dimension(500,480));
chatArea.setBounds(10,10,700,480);
//设置不可编辑
chatArea.setEditable(false);
//添加滚动功能
JScrollPane scrollPane = new JScrollPane(chatArea);
add(scrollPane,BorderLayout.CENTER);//放置界面的中心
add(chatArea);
//刷新列表按钮
JButton jb1 = new JButton("刷新列表");
jb1.addActionListener(bl);
jb1.setBounds(750,30,100,30);
JRadioButton jrb1 = new JRadioButton("群聊");
bg.add(jrb1);
IMG[0] = 1;
JRB[IMG[0]-1] = jrb1;
add(jrb1);
add(jb1);
add(jb);
add(jta);
add(jl);
setVisible(true);
clint = new Clint();
bl.clint = clint;//用户端传过去
bl.ui = this;
bl.chatArea = chatArea;
Thread thread = new Thread(new ChatRunnable(chatArea,clint.getInputStream(),this,bg));
thread.start();
}
@Override
public void paint(Graphics g) {
super.paint(g);
System.out.println("加载列表");
System.out.println("人数:"+IMG[0]);
if(IMG[0] != 0){
for (int i = 0; i <= IMG[0]; i++) {
JRB[i].setBounds(750,80+i*40,100,30);
}
}
}
public static void main(String[] args) {
new ChatUI();
}
}
注:重写paint()方法,对单选按钮进行位置及大小的设置。
同时需要将单选按钮加入一个组 ButtonGroup创建的组中才能实现不能多选的功能。
同时需要按钮的监听,以及一个随时接受消息,并显示在聊天区域的线程。线程在打开UI时就启动。
3.1按钮监听的实现内容:
目前有两个按钮,一个为“发送”,一个为“刷新列表”。
“发送”:发送信息给服务器。获取文本后调用clint中output()的方法。实现发送信息
“刷新列表”:调用repaint()进行重绘。当有新的客户端接入的时候则点击“刷新列表”,即可选择新的客户进行私聊。
package ChatV2;
import ChatV2.Client.Clint;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Arrays;
public class ButtonListener implements ActionListener, DATA {
JTextArea jta;
Clint clint;
JFrame ui;
JTextArea chatArea;
public ButtonListener(JTextArea jta) {
this.jta = jta;
}
@Override
public void actionPerformed(ActionEvent e) {
String ae = e.getActionCommand();
if (ae.equals("发送")) {//按一次发送一次文件
System.out.println("获取文本");
//获取文本,启动用户端
String msg = clint.getPort()+":"+jta.getText();
//获取后清空
jta.setText(null);
//发送信息
int index = groupOrPrivate();
clint.output(msg,index);
System.out.println("下标"+ index);
} else if (ae.equals("刷新列表")) {
System.out.println("刷新列表");
ui.repaint();
}
}
//监控按钮此时为群聊或者私聊
public int groupOrPrivate(){
Boolean[] b = new Boolean[IMG[0]+1];
for (int i = 0; i < b.length; i++) {
b[i] = JRB[i].isSelected();
}
System.out.println(Arrays.toString(b));
for (int i = 0; i < b.length; i++) {
System.out.println("判断私聊or群聊");
if(b[0]){
IMG[1] = 0;//群聊
System.out.println("群聊");
return -1;
} else if (b[i]) {
System.out.println("私聊");
IMG[1] = 1;//私聊
//此时的i,端口。
return i-1;
}
}
System.out.println("判断失败,自动为群聊");
return -1;
}
}
实现了一个判断此时选择的是群聊还是私聊,以及选择私聊的对象的下标。
当为群聊返回-1,私聊则返回其存在DATA接口中端口号在PORT数组中的下标。如果没选则默认为群聊,返回-1;
3.2接受消息的线程:
package ChatV2;
import javax.swing.*;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
public class ChatRunnable implements Runnable, DATA {
InputStream is;
JTextArea chatArea;
JFrame ui;
ButtonGroup bg;
public ChatRunnable(JTextArea chatArea, InputStream is, JFrame ui, ButtonGroup bg) {
this.is = is;
this.chatArea = chatArea;
this.ui = ui;
this.bg = bg;
}
@Override
public void run() {
while (true) {
//接受消息
try {
int ml ;//用于接受字节大小
ml = is.read();
byte[] msgBytes = new byte[ml];//发过来的一串信息
for (int i = 0; i < msgBytes.length; i++) {
int readByte = is.read();
msgBytes[i] = (byte) readByte;
}
String msg = new String(msgBytes, StandardCharsets.UTF_8);
String[] msg1 = msg.split("#");
if (msg1[0].equals("3")) {//为端口记录号
System.out.println("接受消息为端口号");
int size = msg1.length;
int count = 0;
PORT.clear();
for (int i = 1; i < size; i++) {
PORT.add(msg1[i]);
count++;
}
IMG[0] = count;
addUser();
} else {
sendMsg(msg1[0]);
System.out.println("接受到消息:" + msg);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public void sendMsg(String msg) {
chatArea.append(msg + "\n");//发送后换行
chatArea.setCaretPosition(chatArea.getDocument().getLength());//跳转到最新消息
}
//创建新的用户,并存入按钮中
public void addUser() {
for (int i = 1; i <= IMG[0]; i++) {
JRadioButton jrb = new JRadioButton(PORT.get(i-1));
JRB[i] = jrb;
bg.add(jrb);
ui.add(jrb);
}
System.out.println("已有新用户加入,共" + IMG[0] + "个用户连接,请刷新列表");
}
}
方法:
1.sendMsg(String msg);将字符串msg添加到界面的聊天显示界面中,同时调转到发送消息的最下面。
2.addUser();将读取的端口号重新设置在新的单选按钮中,并添加好组,以及添加到界面。
线程功能:接受发送过来的字节,并转为字符串。接着将其用“#”进行分割为字符串数组。
msg1[0]为"3"则为发送的端口号,将后面的进行存储。其他则为私聊或者群聊的消息,调用sendMsg();显示在聊天区域即可。
聊天工具的最终实现效果:
一个用户连接:
第二个用户连接后:
第一个界面刷新前:刷新后:
第三个用户的加入:
聊天功能:
群聊:
私聊:未选择客户端接受不到。
不足:
功能不够齐全。加上文件的发送等功能。
代码的优化。