Fork me on GitHub

使用Java建立聊天客户端

---------------siwuxie095

   

   

   

   

   

   

   

关于 聊天服务器,详见本人博客的分类:来一杯Java

里面的 使用ServerSocket建立聊天服务器(二)

   

本人博客(任选一个)链接:

https://www.baidu.com/s?ie=UTF-8&wd=siwuxie095

   

   

   

使用ServerSocket建立聊天服务器(二) 中的 ChatSocket.java

和 ChatManager.java 略作修改:

   

   

ChatSocket.java:

   

package com.siwuxie095.socket;

   

import java.io.BufferedReader;

import java.io.BufferedWriter;

import java.io.IOException;

import java.io.InputStreamReader;

import java.io.OutputStream;

import java.io.OutputStreamWriter;

import java.io.UnsupportedEncodingException;

import java.net.Socket;

   

/**

* run()循环执行的读取的工作,即当前的服务器会不断的从客户端读取内容

* 将读取到的内容发送到集合Vector中的所有客户端(除了自身)

*

* output()方法中,代码段(1)等效于代码段(2)

*

* @author siwux

*

*/

   

//创建用于Socket通信的线程:ChatSocket

public class ChatSocket extends Thread {

 

Socket socket;

 

//创建构造方法,传入Socket对象

public ChatSocket(Socket socket) {

this.socket=socket;

}

 

 

 

public void output(String out) {

 

//(1)

// try {

// socket.getOutputStream().write((out+"\n").getBytes("UTF-8"));

// } catch (UnsupportedEncodingException e) {

// e.printStackTrace();

// } catch (IOException e) {

// System.out.println("断开了一个客户端连接");

// ChatManager.getChatManager().remove(this);

// e.printStackTrace();

// }

 

 

//(2)

try {

   

//对当前的Socket执行 数据输出相关功能的包装

//使用getOutputStream()获取输出流,通过输出流向外输出数据

//返回值是OutputStream类型,创建以接收返回值

OutputStream os=socket.getOutputStream();

 

   

//创建一个BufferedWriter作为数据的输出,传入匿名对象,指定编码,层层包装

BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(os,"UTF-8"));

   

//BufferedWriter输出字符串

//同时加一个换行符,表示一行已完结,这样flush时才会强制输出

//如果不加,数据只会传到当前socket的缓冲区,而发不到其他的客户端的界面上

bw.write(out+"\n");

//因为带缓冲,所以需要强制输出,不然无法输出

bw.flush();

   

}catch (UnsupportedEncodingException e) {

e.printStackTrace();

} catch (IOException e) {

System.out.println("断开了一个客户端连接");

ChatManager.getChatManager().remove(this);

e.printStackTrace();

}

   

}

 

//复写run()方法

@Override

public void run() {

 

output("你已经连接到了服务器");

 

try {

 

//Socket的输入流进行包装

//在指定InputStreamReader时,指定编码的字符集

//有异常抛出,用 try catch 捕获

BufferedReader br=new BufferedReader(

new InputStreamReader(

socket.getInputStream(),"UTF-8"));

 

String line=null;

//读取从客户端发送给服务器的数据

while ((line=br.readLine())!=null) {

System.out.println(line);

//通过静态方法,将自己传入,同时传入line

ChatManager.getChatManager().publish(this, line);

}

 

//关闭流

br.close();

 

System.out.println("断开了一个客户端连接");

ChatManager.getChatManager().remove(this);

 

} catch (IOException e) {

System.out.println("断开了一个客户端连接");

ChatManager.getChatManager().remove(this);

e.printStackTrace();

}

 

 

}

}

   

   

   

ChatManager.java:

   

package com.siwuxie095.socket;

   

import java.util.Vector;

   

   

/**

* ChatManager 将不同的socket所新建的ChatSocket线程管理起来

* 由于一个聊天服务器只能有一个聊天的管理器:ChatManager

* 所以要把这个类作单例化处理

* 单例化的第一步就是让这个类的构造方法变成 private 类型

*

* @author siwux

*

*/

public class ChatManager {

 

//让构造方法变成private类型,完成单例化的第一步

private ChatManager(){}

 

//为当前类创建一个实例

private static final ChatManager cm=new ChatManager();

 

//创建方法 getChatManager(),返回ChatManager类型

public static ChatManager getChatManager() {

return cm;

}

 

 

 

//至此,完成了这个类(ChatManager)的单例化

//接下来就是对ChatSocket线程进行管理

 

 

//创建一个Vector,指定泛型为ChatSocket

Vector<ChatSocket> vector=new Vector<>();

 

//创建add()方法,为当前集合添加一个新的ChatSocket对象

//在创建ChatSocket时使用,即ServerListener.java中使用

public void add(ChatSocket cs) {

//调用Vectoradd()方法,传入cs即可

vector.add(cs);

}

 

 

public void remove(ChatSocket cs) {

vector.remove(cs);

}

 

 

//创建publish()方法,其中的某个ChatSocket线程可以调用publish()

//向其他的客户端(其他的ChatSocket线程)发送信息

//传入线程本身和需要发送的信息

public void publish(ChatSocket cs,String msg) {

 

//因为要发送给集合Vector中的其他所有线程,要使用遍历

for (int i = 0; i < vector.size(); i++) {

//获取循环中的第 i 个对象

ChatSocket csx=vector.get(i);

 

//当前发送信息的线程就不用再接收这条信息,

//判断发送消息的对象是不是当前对象

if (!cs.equals(csx)) {

csx.output(msg);

}

}

}

 

 

 

 

 

 

   

}

   

   

   

   

   

   

聊天客户端:

   

工程名:MyJavaChatClient

包名:com.siwuxie095.main、com.siwuxie095.view

类名:StartClient.java(主类)、ChatManagerX.java、MainWindow.java

   

   

工程结构目录如下:

   

   

   

   

   

StartClient.java(主类):

   

package com.siwuxie095.main;

   

import java.awt.EventQueue;

   

import com.siwuxie095.view.MainWindow;

   

public class StartClient {

   

public static void main(String[] args) {

 

//将窗体MainWindow.java的主方法里的代码复制粘贴到这里(StartClient.java)

//MainWindow.java里的主方法删除,这样就可以在主程序中将窗体执行出来了

EventQueue.invokeLater(new Runnable() {

public void run() {

try {

MainWindow frame = new MainWindow();

frame.setVisible(true);

//在创建Window时传递对MainWindow的引用

ChatManagerX.getCM().setWindow(frame);

} catch (Exception e) {

e.printStackTrace();

}

}

});

 

}

   

}

   

   

   

ChatManagerX.java:

   

package com.siwuxie095.main;

   

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

import java.io.OutputStreamWriter;

import java.io.PrintWriter;

import java.net.Socket;

import java.net.UnknownHostException;

   

import com.siwuxie095.view.MainWindow;

   

//需要将这个类单例化

public class ChatManagerX {

   

//让构造方法变成private类型,完成单例化的第一步

private ChatManagerX(){}

 

//创建一个 private static final 修饰的实例

private static final ChatManagerX instance=new ChatManagerX();

 

//创建公有的 getChatManager()获取上面的私有实例

//这样,当前的ChatManager类就只会有唯一的一个实例

public static ChatManagerX getCM() {

return instance;

}

 

 

//现在界面中只能向ChatManager发送数据

//为了让接收到的数据放到界面上,最简便的方法即在本地引用MainWindow

MainWindow window;

//实现setWindow()方法

public void setWindow(MainWindow window) {

this.window = window;

//ChatManager中操作window.appendText()在界面中显示数据

window.appendText("提示:文本框已经和ChatManager绑定了");

}

 

 

//创建一个Socket对象

Socket socket;

//创建输入流 BufferedReader

BufferedReader reader;

//创建输出流 PrintWriter 这里不使用 BufferedWriter

PrintWriter writer;

 

//具体实现连接服务器的操作

public void connect(String ip) {

 

//呼叫连接时创建一个新的线程

//对网络的处理放到线程中执行

//(先建立连接,之后循环从服务器读取数据)

new Thread(){

 

 

@Override

public void run() {

 

try {

//传入ip 并设置端口为 12345

socket=new Socket(ip, 12345);

 

//获取输入和输出流,层层包装

writer=new PrintWriter(

new OutputStreamWriter(

socket.getOutputStream(),"UTF-8"));

 

reader=new BufferedReader(

new InputStreamReader(

socket.getInputStream(),"UTF-8"));

 

//创建完输入输出流,需要循环监听当前输入流是否有数据

String line;

while ((line=reader.readLine())!=null) {

//通过window.appendText()将数据输出到界面中

window.appendText("收到:"+line);

}

 

//读取数据为空,则连接结束,关闭流

writer.close();

reader.close();

//置空,更安全

writer=null;

reader=null;

 

} catch (UnknownHostException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}

}

   

 

 

}.start();

}

 

//点击发送按钮时的操作

public void send(String out) {

//发送时,writer不为空才能发送

if (writer!=null) {

//为当前的socket输出内容时,需要添加 \n 进行换行

//这时flush才能强制把这一行输出,

//即这一行完结了,flush才会把它输出

//

//如果不加 \n 数据实际上是传出去了,

//但只是传到了当前socket的缓冲区域

//只有关闭了socketsocket才会把它强制输出,这是不可取的

//因为聊天要保证实时性,所以要加一个换行符

writer.write(out+"\n");

//flush强制刷新输出

writer.flush();

}else {

//提示信息

window.appendText("提示:当前的连接已经中断...");

}

 

 

}

}

   

   

   

MainWindow.java:

   

package com.siwuxie095.view;

   

import java.awt.event.ActionEvent;

import java.awt.event.ActionListener;

import java.awt.event.MouseAdapter;

import java.awt.event.MouseEvent;

   

import javax.swing.GroupLayout;

import javax.swing.GroupLayout.Alignment;

import javax.swing.JButton;

import javax.swing.JFrame;

import javax.swing.JPanel;

import javax.swing.JTextArea;

import javax.swing.JTextField;

import javax.swing.LayoutStyle.ComponentPlacement;

import javax.swing.border.EmptyBorder;

   

import com.siwuxie095.main.ChatManagerX;

   

/**

* 添加 默认序列版本ID

* 添加 一个TextAreaJTextArea

* 选择根容器 contentPane,将布局改为 GroupLayout

*

* JTextArea 上下方各添加一个JTextFieldJButton

*

* @author siwux

*

*/

public class MainWindow extends JFrame {

   

/**

* 添加默认序列版本ID

* Add default serial version ID

*/

private static final long serialVersionUID = 1L;

private JPanel contentPane;

 

//将文本控件等声明到类当中,作为整个类的变量

JTextArea txt;

private JTextField ip;

private JTextField send;

   

 

   

/**

* Create the frame.

*/

public MainWindow() {

//添加属性 setAlwaysOnTop(true)

//这样打开多个窗口都可以显示在其他应用的上方

setAlwaysOnTop(true);

 

setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

setBounds(100, 100, 504, 353);

contentPane = new JPanel();

contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));

setContentPane(contentPane);

 

txt = new JTextArea();

txt.setText("Ready...");

 

ip = new JTextField();

ip.setText("127.0.0.1");

ip.setColumns(10);

 

JButton button = new JButton("连接到服务器");

button.addMouseListener(new MouseAdapter() {

@Override

public void mouseClicked(MouseEvent arg0) {

//使用ChatManagerconnect()和按钮的监听事件绑定

//通过ip.getText()获得需要连接的服务器的IP地址

//通过IP来建立Socket连接

ChatManagerX.getCM().connect(ip.getText());

 

}

});

button.addActionListener(new ActionListener() {

public void actionPerformed(ActionEvent arg0) {

}

});

 

send = new JTextField();

send.setText("你好");

send.setColumns(10);

 

JButton btnNewButton = new JButton("发送");

btnNewButton.addMouseListener(new MouseAdapter() {

@Override

public void mouseClicked(MouseEvent arg0) {

//使用ChatManagersend()和按钮的监听事件绑定

//传入send文本框的文本

//第一个send()到服务器

//第二个appendText()添加到中间的显示界面

ChatManagerX.getCM().send(send.getText());

appendText("我说:"+send.getText());

//当前文本发送出去后,需要将当前的文本框清空

send.setText("");

 

}

});

btnNewButton.addActionListener(new ActionListener() {

public void actionPerformed(ActionEvent e) {

}

});

 

 

//下面这一大段是自动生成的代码,不用管

GroupLayout gl_contentPane = new GroupLayout(contentPane);

gl_contentPane.setHorizontalGroup(

gl_contentPane.createParallelGroup(Alignment.LEADING)

.addGroup(gl_contentPane.createSequentialGroup()

.addContainerGap()

.addGroup(gl_contentPane.createParallelGroup(Alignment.LEADING)

.addComponent(txt, GroupLayout.DEFAULT_SIZE, 417, Short.MAX_VALUE)

.addGroup(gl_contentPane.createSequentialGroup()

.addComponent(send, GroupLayout.PREFERRED_SIZE, 302, GroupLayout.PREFERRED_SIZE)

.addPreferredGap(ComponentPlacement.RELATED)

.addComponent(btnNewButton, GroupLayout.DEFAULT_SIZE, 109, Short.MAX_VALUE))

.addGroup(gl_contentPane.createSequentialGroup()

.addComponent(ip, GroupLayout.PREFERRED_SIZE, 321, GroupLayout.PREFERRED_SIZE)

.addPreferredGap(ComponentPlacement.RELATED)

.addComponent(button, GroupLayout.PREFERRED_SIZE, 90, Short.MAX_VALUE)))

.addContainerGap())

);

gl_contentPane.setVerticalGroup(

gl_contentPane.createParallelGroup(Alignment.LEADING)

.addGroup(gl_contentPane.createSequentialGroup()

.addGroup(gl_contentPane.createParallelGroup(Alignment.BASELINE)

.addComponent(ip, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)

.addComponent(button))

.addPreferredGap(ComponentPlacement.RELATED)

.addComponent(txt, GroupLayout.DEFAULT_SIZE, 201, Short.MAX_VALUE)

.addPreferredGap(ComponentPlacement.RELATED)

.addGroup(gl_contentPane.createParallelGroup(Alignment.BASELINE)

.addComponent(send, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)

.addComponent(btnNewButton)))

);

contentPane.setLayout(gl_contentPane);

}

 

 

 

//为当前类创建一个appendText()方法,传入一个String

public void appendText(String in) {

//添加到中间的显示界面

txt.append("\n"+in);

}

}

   

   

先将服务器运行:

   

   

   

   

再运行客户端:

   

   

   

   

客户端界面:

   

   

   

   

点击 连接到服务器,服务器 弹出提示:

   

   

   

   

点击 确定,客户端弹出服务器发出的消息:

   

   

   

   

再次运行另一个客户端,两个客户端之间发送消息:

   

   

   

   

   

此时,服务器的控制台也会打印所有经过服务器的消息:

   

   

   

   

   

整个流程:

   

两个客户端 通过 IP 和 端口 向服务器发起 Socket 通信请求,

服务器监听到请求并同意

   

两个客户端就是两个 Socket 对象,服务器创建两个 ChatSocket 线程,

分别处理这两个连接,且这两个线程由 ChatManager 管理

   

这两个 ChatSocket 线程先向两个客户端发出消息:"你已经连接

到了服务器",实际上这是服务器发送给客户端的消息

   

客户端之间发送消息:客户端 A 发出消息后,服务器接收到该消息,

并将其转发到客户端 B,即 服务器 相当于 中转站

   

如果有多个客户端,服务器依然是中转站,将一个客户端的消息转发

给其他客户端(相当于 群聊)

   

   

   

   

   

【made by siwuxie095】

posted on 2017-04-01 15:11  siwuxie095  阅读(325)  评论(0编辑  收藏  举报

导航