1.基本架构
2.服务端
3.客户端
4.运行效果
一,基本架构
1.客户端和服务端通过 socket套接字来通信
2.客户端只有一类socket套接字,专门用于处理与服务端的通信的
3.服务端有两类socket套接字,一个是专门用于监听有没有客户端的连接请求,另一个是专门用于处理与客户端的通信。前者和后者独立运行,用多线程实现。
4.当服务端监听到有客户端的连接请求,服务端就会新建一个新线程,专门用于服务这个客户端。
5.服务端维护一个客户端列表,用于记录连接进来的客户端。当服务器收到一个客户端发来的信息,就会遍历客户端列表,把信息发给所有客户端,以实现群聊的功能。
二,服务端
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.net.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ChatServer {
ServerSocket ss = null; //服务器的的套接字,用于监听来自客户端的连接请求
boolean started = false;
List<Client> clients = new ArrayList<Client>();//维护一个客户端链表,用于记录已经连接上服务端的客户端们
/*主程序*/
public static void main(String[] args) {
new ChatServer().start(); //调用本类的start()方法。因为服务端的操作在 start()方法中。
}
/*服务端的操作*/
public void start(){
try {
ss = new ServerSocket(8888); //指定服务端套接字的端口号为8888(可以任意指定,但必须大于1024)
started = true;
} catch (BindException e) { //若捕捉到此异常,表明指定的端口号已经被占用了
System.out.println("端口使用中..请重新启动程序");
System.exit(0); //程序退出
} catch (IOException e) {
e.printStackTrace();
}
try{
while(started){
Socket s = ss.accept(); //创建一个用于管理客户端连接的客户端套接字。每当有新的客户端连接,服务端就新建一个套接字给此客户端
Client c = new Client(s);//每当一个客户端连上服务端,就新建一个Client类,指代此客户端,并传入客户端套接字
new Thread(c).start(); //新建一个子线程,并启动它
clients.add(c); //把Client对象加入到客户端列表中
}
} catch(IOException e){
e.printStackTrace();
} finally{
try {
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/*Client类实现Runnable接口,为一个线程类。*/
class Client implements Runnable {
Socket s = null; //客户端套接字的引用
DataInputStream dis = null; //输入流的引用
DataOutputStream dos = null; //输出流的引用
boolean bConnected = false; //服务端是否已经获取客户端的输入流,输出流
public Client(Socket s) { //Client类的带参构造函数
this.s = s; //Client类中的客户端套接字引用指向 传入的客户端套接字
try {
dis = new DataInputStream(s.getInputStream()); //getInoutStream()用于获取客户端套接字自带的输入流
dos = new DataOutputStream(s.getOutputStream());//getOutoutStream()用于获取客户端套接字自带的输出流
bConnected = true; //成功获取客户端的输入流,输出流后,把bConnected设置为true
} catch (IOException e) {
e.printStackTrace();
}
}
/*用于向客户端发送信息*/
public void send(String str){
try {
dos.writeUTF(str);//向输入流发送信息
} catch (IOException e) { //当此Client对象出现异常,把此Client从客户端列表clients中删除
clients.remove(s);
}
}
/*Client线程的操作*/
public void run() {
String str = null;
try{
while(bConnected){ //当成功获取客户端的 输入流,输出流后。
str = dis.readUTF(); //从输入流读取来自客户端的信息
//System.out.println(str);
for(int i=0;i<clients.size();i++){ //从一个客户端中获取信息后,发此信息发送给已经连接上服务端的其他客户端
Client c = clients.get(i); //用列表序号来获取对应的客户端
c.send(str); //向以序号遍历的客户端发送信息
}
}
} catch (EOFException e){ //由于客户端关闭了的时候,服务端不能从流中读取信息,就会报此异常。表示客户端关闭了
System.out.println("Clinet closed!");
} catch (IOException e){
e.printStackTrace();
}
finally{
try {
if(s!=null) s.close(); //关闭客户端套接字
if(dos!=null) dos.close();//关闭此客户端的输出流
if(dis!=null) dis.close();//关闭此客户端的输入流
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
3.客户端
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
public class ChatClient extends Frame { //继承Frame类,用于给ChatClient类创建界面
Socket s = null; //用于连接服务端的套接字
DataInputStream dis = null; //用于获取来自服务端的信息的输入流
DataOutputStream dos = null; //用于向服务端发送信息的输出流
boolean bConnected = false; // 连接服务端的标志位
TextField tfTxt = new TextField(); //创建一个文本框(放在界面中)
TextArea taContent = new TextArea();//创建一个消息框(放在界面中)
public static void main(String[] args) { //主函数
new ChatClient().lauchFrame(); //lauchFrame()用于设置界面布局
}
/*设置界面布局*/
public void lauchFrame() {
this.setLocation(400, 300); //界面在电脑屏幕中的显示位置
this.setSize(300, 300); //界面大小(长,宽)
add(tfTxt, BorderLayout.SOUTH); //把已创建的文本框加入到界面中的南方(即下方)
add(taContent, BorderLayout.NORTH); //把已创建的消息框加入到界面中的北方(即上方)
this.addWindowListener(new WindowAdapter() { //加入window监听器到界面中
public void windowClosing(WindowEvent e) { //当鼠标点击右上角的'X'时,会自动调用windowClosing()
disconnect(); //调用disconnect()函数,用于断开与服务端的连接
System.exit(0); //程序退出
}
});
pack(); //将界面上的控件位置自适应放好
tfTxt.addActionListener(new TFListener()); //加入Action监听器到文本框中
setVisible(true); //使界面显示出来
connect(); //调用connect(),用于连接服务端
new Thread(new RecvThread()).start(); //创建一个新线程,用于接收来自服务端的信息
}
/*用于连接服务端*/
public void connect() {
try {
s = new Socket("127.0.0.1", 8888); //创建套接字对象,第一个参数为服务端所在的ip地址,后一个参数为服务端的端口号
dis = new DataInputStream(s.getInputStream()); //创建输入流,用于接收服务端的信息
dos = new DataOutputStream(s.getOutputStream()); //创建输出流,用于向服务端发送信息
bConnected = true; //设置 连接服务端的标志位 为真
System.out.println("Client connected!");
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/*当要断开与服务端的连接时,作一些处理*/
public void disconnect() {
try {
dos.close(); //断开输出流
dis.close(); //断开输入流
s.close(); //断开套接字
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/*自定义Action监听器的内容*/
private class TFListener implements ActionListener {
public void actionPerformed(ActionEvent e) {//实现ActionListener接口中的actionPerformed()方法
String str = tfTxt.getText().trim(); //当按下回车后,获取文本框中的信息。trim()用于消除文本信息前面和后尾的空格
tfTxt.setText(""); //(发送信息后)把文本框置空。
try {
dos.writeUTF(str); //往输出流写数据(用于发信息给服务端)
dos.flush(); //强制把流缓冲区里的信息发出去。(否则系统会等到缓冲区满了以后才会发出去)
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
/*自定义用于接收来自服务端的信息的子线程*/
private class RecvThread implements Runnable{
@Override
public void run() { //子线程工作的内容都写在 Runnable接口的 run()方法里
try{
while(bConnected){ //当bConnected 为true时,表示客户端连接服务端成功
String str = dis.readUTF(); //从输入流中获取来自服务端的信息
taContent.setText(taContent.getText()+str+'\n'); //把来自服务端的信息加上消息框以前的信息一起在消息框中打印出来
//System.out.println(str);
}
} catch(IOException e){ //当出现IO异常时,很可能是因为客户端程序被用户关闭了,此时打印“client close”
System.out.println("client close!");
}
}
}
}
目录树:
4.运行结果:
先开服务端程序,再开多个客户端:
如图所示:
开了3个客户端,当从其中一个客户端中输入信息时,其他的客户端也会收到。