学完了socket通讯后,在老师的要求下,写了一个仿qq的聊天程序:
最终调试程序结果如下图: 有bug希望提出来,我们一起解决。
设计思路:
在服务器端 用一个HashMap
(3)发送消息
msg @# 消息发送者 @# 消息内容
代码实现:
客户端:
package cn.hncu.net.sina;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.border.TitledBorder;
public class ClientFrom extends JFrame implements ActionListener{
private static String ip="127.0.0.1";
private static int port=8080;
private JTextField tfdUserName=new JTextField(10); //用户标识
private JTextArea allMsg=new JTextArea(); //聊天信息显示
private JTextField tfdMsg=new JTextField(10);//发送消息消息框
private JButton btnSend; //发送消息按钮
private JButton btnCon;
//在线用户列表
private DefaultListModel dataModel=new DefaultListModel();
private JList list=new JList(dataModel);
public ClientFrom() {
setBounds(300,300,400,300);
addMenuBar(); //添加菜单
////////////////////上方面板/////////////
JPanel northPanel=new JPanel();
northPanel.add(new JLabel("用户名称"));
tfdUserName.setText("");
northPanel.add(tfdUserName);
btnCon=new JButton("连接");
btnCon.setActionCommand("c");
JButton btnExit=new JButton("退出");
btnExit.setActionCommand("exit");
northPanel.add(btnCon);
northPanel.add(btnExit);
getContentPane().add(northPanel,BorderLayout.NORTH); //放在上方
//////////////////中间面板////////////////
JPanel centerPanel=new JPanel(new BorderLayout());
//中
allMsg=new JTextArea();
allMsg.setEditable(false);
allMsg.setForeground(Color.blue);
allMsg.setFont(new Font("幼圆", Font.BOLD, 14));
centerPanel.add(new JScrollPane(allMsg));
//东
dataModel.addElement("全部");
list.setSelectedIndex(0); //设置默认选择位置
list.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION); //设置只能单选
list.setVisibleRowCount(5); //设置显示的行数
list.setFont(new Font("幼圆", Font.BOLD, 12));
JScrollPane scroll=new JScrollPane(list); //为list添加滚动条
scroll.setBorder(new TitledBorder("在线")); //Border的实现类TitileBorder
scroll.setPreferredSize(new Dimension(70, allMsg.getHeight())); //设置滚动条的首选大小
centerPanel.add(scroll,BorderLayout.EAST);
//南
JPanel southPanel=new JPanel();
southPanel.add(new JLabel("消息"));
southPanel.add(tfdMsg);
btnSend=new JButton("发送");
btnSend.setActionCommand("send");
btnSend.setEnabled(false);
southPanel.add(btnSend);
centerPanel.add(southPanel,BorderLayout.SOUTH);
//把中间面板加到框架中
getContentPane().add(centerPanel);
//事件监听
btnCon.addActionListener(this);
btnExit.addActionListener(this);
btnSend.addActionListener(this);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
if(tfdUserName.getText()==null || tfdUserName.getText().trim().length()==0){
int result = JOptionPane.showConfirmDialog(ClientFrom.this, "你还没登录,是否退出");
if(result==JOptionPane.YES_OPTION){
System.exit(0);
}else{
return;
}
}
System.out.println(tfdUserName.getText()+"退出");
sendExitMsg();
System.exit(0);
}
});
setVisible(true);
}
private void addMenuBar() {
JMenuBar menuBar=new JMenuBar();
setJMenuBar(menuBar);
JMenu menu=new JMenu("选项");
menuBar.add(menu);
JMenuItem itemSet=new JMenuItem("设置");
JMenuItem itemHelp=new JMenuItem("帮助");
itemSet.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
final JDialog setDlg=new JDialog(ClientFrom.this);
setDlg.setBounds(ClientFrom.this.getX(), ClientFrom.this.getY(), 250, 100);
setDlg.setLayout(new FlowLayout());
setDlg.add(new JLabel("服务器:"));
final JTextField tfdIP=new JTextField(10);
tfdIP.setText(ip);
setDlg.add(tfdIP);
setDlg.add(new JLabel("端口:"));
final JTextField tfdPort=new JTextField(10);
tfdPort.setText(port+"");
setDlg.add(tfdPort);
JButton btnSet=new JButton("设置");
btnSet.setActionCommand("set");
JButton btnCanel=new JButton("取消");
btnCanel.setActionCommand("canel");
setDlg.add(btnSet);
setDlg.add(btnCanel);
btnSet.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if("set".equals(e.getActionCommand())){
if(tfdIP.getText()!=null && tfdIP.getText().trim().length()>0){
ClientFrom.this.ip=tfdIP.getText();
}
if(tfdPort.getText()!=null && tfdPort.getText().trim().length()>0){
try {
ClientFrom.this.port=Integer.parseInt(tfdPort.getText());
} catch (NumberFormatException e1) {
JOptionPane.showMessageDialog(setDlg, "端口号格式输入错误,请输入数字");
}
}
btnCon.setEnabled(true);
tfdUserName.setEditable(true);
if(client!=null){
//如果前面已经登录着用户,就把用户退出
String msg="exit@#全部@#null@#"+tfdUserName.getText();
pw.println(msg);
dataModel.removeElement(tfdUserName.getText());
list.validate();
tfdUserName.setText("");
}
setDlg.dispose();
}else if("canel".equals(e.getActionCommand())){
return;
}
}
});
setDlg.setVisible(true);
}
});
itemHelp.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JDialog helpDlg = new JDialog(ClientFrom.this);
helpDlg.setBounds(ClientFrom.this.getX()+10, ClientFrom.this.getY(), 300, 100);
JLabel str = new JLabel("版权所有@dragon_Dai.QQ:794530831");
helpDlg.add(str);
helpDlg.setVisible(true);
}
});
menu.add(itemSet);
menu.add(itemHelp);
}
@Override
public void actionPerformed(ActionEvent e) {
if("c".equals(e.getActionCommand())){
System.out.println(tfdUserName.getText());
if(tfdUserName.getText()==null || tfdUserName.getText().trim().length()==0){
JOptionPane.showMessageDialog(this, "用户名不能为空");
return;
}
System.out.println(tfdUserName.getText()+":连接ing...");
connecting();
}else if("exit".equals(e.getActionCommand())){
if(tfdUserName.getText()==null || tfdUserName.getText().trim().length()==0){
int result = JOptionPane.showConfirmDialog(this, "你还没登录,是否退出");
if(result==JOptionPane.YES_OPTION){
System.exit(0);
}else{
return;
}
}
System.out.println(tfdUserName.getText()+"退出");
sendExitMsg();
}else if("send".equals(e.getActionCommand())){
if(tfdMsg.getText()==null){
JOptionPane.showMessageDialog(this, "发送消息不能为空");
return;
}
String msg="on@#"+list.getSelectedValue()+"@#"+tfdMsg.getText()+"@#"+tfdUserName.getText();
pw.println(msg);
}
}
private Socket client;
private PrintWriter pw;
private void connecting() {
//与服务器建立连接,把userName传给服务器
try {
client=new Socket(ip,port);
//发送用户名给服务器
btnCon.setEnabled(false); //连接成功后关掉连接按钮
String userName=tfdUserName.getText().trim();
pw=new PrintWriter(client.getOutputStream(),true);
pw.println(userName);
//连接之后,设置标题为userName在线
setTitle(userName+"在线");
btnSend.setEnabled(true); //打开发送按钮
tfdUserName.setEditable(false); //用户名不能再修改
//开一个线程单独用于跟服务器通信
new ClientThread(client).start();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
private void sendExitMsg() {
//与服务器建立连接,把userName传给服务器
try {
client=new Socket(ip, port);
String msg="exit@#全部@#null@#"+tfdUserName.getText();
pw.println(msg);
System.exit(0);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
class ClientThread extends Thread{
private Socket client;
public ClientThread(Socket client) {
this.client=client;
}
@Override
public void run() {
//接收服务器返回的信息
try {
Scanner sc=new Scanner(client.getInputStream());
while(sc.hasNext()){
String msg=sc.nextLine();
String msgs[]=msg.split("@#");
if(msgs==null || msgs.length!=3){
System.out.println("通讯异常");
return;
}
if("msg".equals(msgs[0])){
//表示该信息是用来显示用的
if("server".equals(msgs[1])){
//表示该信息是系统信息
msg="系统信息:"+msgs[2];
allMsg.append(msg+"\r\n");
}else{
//表示该信息聊天信息
msg=msgs[1]+msgs[2];
allMsg.append(msg+"\r\n");
}
}else if("cmdAdd".equals(msgs[0])){
//表示该消息是用来更新用户在线列表的,添加用户
dataModel.addElement(msgs[2]);
}else if("cmdRed".equals(msgs[0])){
//表示该消息是用来更新用户在线列表的,移除用户
dataModel.removeElement(msgs[2]);
}
list.validate(); //需要刷新list,不然可能出现list更新失败的bug
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
JFrame.setDefaultLookAndFeelDecorated(true);
new ClientFrom();
}
}
服务器:
package cn.hncu.net.sina;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Scanner;
import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.border.TitledBorder;
public class ServerFrom extends JFrame{
private JTextArea area;//在线的用户信息显示
private DefaultListModel dataModel; //在线的用户列表显示
//注册的用户名不能相同
//用于存储所有的用户,这里采用注册的"用户名"做key值,通信的socket做value值
private Map userMap=new HashMap();
public ServerFrom() {
setTitle("聊天服务器");
setDefaultCloseOperation(EXIT_ON_CLOSE);
Toolkit toolkit=Toolkit.getDefaultToolkit();
Dimension dim=toolkit.getScreenSize();
int runWidth=500;
int runHeight=400;
int width=(int) dim.getWidth();
int height=(int) dim.getHeight();
//设置界面居中显示
setBounds(width/2-runWidth/2, height/2-runHeight/2, runWidth, runHeight);
area=new JTextArea();
area.setEditable(false);
getContentPane().add(new JScrollPane(area),BorderLayout.CENTER);
//列表显示
dataModel=new DefaultListModel();
JList list=new JList(dataModel);
JScrollPane scroll=new JScrollPane(list);
scroll.setBorder(new TitledBorder("在线"));
scroll.setPreferredSize(new Dimension(100, this.getHeight()));
getContentPane().add(scroll,BorderLayout.EAST);
//菜单
JMenuBar menuBar=new JMenuBar();
setJMenuBar(menuBar);
JMenu menu=new JMenu("控制(C)");
menu.setMnemonic('C'); //设置快捷键为 Alt+C
menuBar.add(menu);
//开启
final JMenuItem itemRun=new JMenuItem("开启");
//快捷键 Ctrl+R
itemRun.setAccelerator(KeyStroke.getKeyStroke('R', KeyEvent.CTRL_MASK));
itemRun.setActionCommand("run");
menu.add(itemRun);
//退出
JMenuItem itemExit=new JMenuItem("退出");
itemExit.setAccelerator(KeyStroke.getKeyStroke('E', KeyEvent.CTRL_MASK));
itemExit.setActionCommand("exit");
menu.add(itemExit);
itemRun.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if("run".equals(e.getActionCommand())){
startServer();
itemRun.setEnabled(false);
}
}
});
setVisible(true);
}
private void startServer() {
try {
System.out.println("服务器启动");
ServerSocket server=new ServerSocket(8080);
area.append("启动服务器:"+server);
//单独开启一个线程用于与客户端握手
new ServerThread(server).start();
} catch (IOException e) {
e.printStackTrace();
}
}
class ServerThread extends Thread{
private ServerSocket server;
public ServerThread(ServerSocket server) {
this.server=server;
}
@Override
public void run() {
try {
while(true){
Socket s=server.accept();
//读取客户端第一次向服务器请求的信息
// BufferedReader br=new BufferedReader(new InputStreamReader(s.getInputStream()));
// if(br.readLine()!=null){
// String userName=br.readLine();
// }
Scanner sc=new Scanner(s.getInputStream());
if(sc.hasNext()){
String userName=sc.next();
area.append("\r\n"+userName+"上线了。"+s);
dataModel.addElement(userName);
// userMap.put(userName, s); //在后面在把这个用户加入到集合中好一点,那样发送上线信息给所有用户时,就不用判断不发发给自己了。
//登录成功
//在专门开一个线程用于跟针对某一个客户端通讯
//根据接收客户端发来的协议判断,客户端进行的是什么样的请求
new ClientThread(s).start();
//告诉其他用户有人上线了
sendMsgToAll(userName);
//把消息其他在线的用户的信息传给登录的这个客户端
sendMsgToSelf(s);
userMap.put(userName, s);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void sendMsgToAll(String userName) throws IOException{ //这里的异常可以抛,因为调用这个方法的位置抓了IOException
//遍历map中所有除了该用户之外的客户--此时登录的用户还没有加入到容器中,所有可以直接遍历所有用户
Iterator it = userMap.values().iterator();
while(it.hasNext()){
Socket s=it.next();
PrintWriter pw=new PrintWriter(s.getOutputStream(),true);
//服务器向客户端发的消息格式设计:
//命令关键字@#发送方@#消息内容
String msg="msg@#server@#"+userName+"登录了"; //用于显示用的.
pw.println(msg);
msg="cmdAdd@#server@#"+userName; //用于给客户端维护在线用户列表用的
pw.println(msg);
// pw.close();
// s.close();
}
}
public void sendMsgToSelf(Socket s) throws IOException{
PrintWriter pw = new PrintWriter(s.getOutputStream(),true);
Iterator it = userMap.keySet().iterator();
while(it.hasNext()){
String userName=it.next();
System.out.println("map:"+userMap);
//告诉用户当前在线用户信息,不需要发送显示信息,只需要发送给客户端更新在线列表的信息
String msg="cmdAdd@#server@#"+userName;
pw.println(msg);
}
// pw.close();
}
//专门用于跟某一个用户通讯的线程
class ClientThread extends Thread{
private Socket s;
public ClientThread(Socket s) {
this.s=s;
}
@Override
public void run() {
try {
//根据接收客户端发来的协议判断,客户端进行的是什么样的请求
Scanner sc=new Scanner(s.getInputStream());
while(sc.hasNextLine()){
String msg=sc.nextLine();
String msgs[]=msg.split("@#");
//简单防黑。
if(msgs==null || msgs.length!=4){
System.out.println("通讯异常:"+msg);
return;
}
if("on".equals(msgs[0])){//表示客户端的请求是:向别人发送信息
sendMsgToSb(msgs);
}else if("exit".equals(msgs[0])){//表示客户端发送的请求是:退出(下线)
area.append("\r\n"+msgs[3]+"下线了"+s);
dataModel.removeElement(msgs[3]);
userMap.remove(msgs[3]);
//通知其他所有在线的用户,***退出了
sendSbExitMsgToAll(msgs);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
//命令关键字@#接收方@#消息内容@#发送方
public void sendMsgToSb(String[] msgs) throws IOException {
//可能是发给所有人,也可能是发给某一个人
if("全部".equals(msgs[1])){
//发给所有人(群聊)
Iterator it = userMap.keySet().iterator();
while(it.hasNext()){
String userName=it.next();
String msg=null;
if(userName.equals(msgs[3])){
msg="msg@#"+"我"+"@#说:"+msgs[2];
}else{
msg="msg@#"+msgs[3]+"@#说:"+msgs[2];
}
Socket s=userMap.get(userName);
//msg@#消息发送者@#消息内容
PrintWriter pw=new PrintWriter(s.getOutputStream(), true);
pw.println(msg);
}
}else{
//发送给某一个人
String userName=msgs[1];
Socket s=userMap.get(userName);
//msg@#消息发送者@#消息内容
String msg="msg@#"+msgs[3]+"@#悄悄对你说:"+msgs[2];
PrintWriter pw=new PrintWriter(s.getOutputStream(), true);
pw.println(msg);
//在发给自己
Socket s2 = userMap.get(msgs[3]);
PrintWriter pw2 = new PrintWriter(s2.getOutputStream(), true);
String str2 = "msg@#"+"我"+"@#对 "+userName+"说:"+msgs[2];
pw2.println(str2);
}
}
//通知其他所有在线的用户,***退出了
//1) msg @# server @# 用户[userName]退出了 (给客户端显示用的)
//2) cmdRed@#server @# userName (给客户端维护在线用户列表用的)
public void sendSbExitMsgToAll(String[] msgs) throws IOException {
Iterator it=userMap.keySet().iterator();
while(it.hasNext()){
String userName=it.next();
Socket s=userMap.get(userName);
PrintWriter pw=new PrintWriter(s.getOutputStream(), true);
String msg="msg@#server@#用户["+msgs[3]+"]退出了";
pw.println(msg);
msg="cmdRed@#server@#"+msgs[3];
pw.println(msg);
}
}
public static void main(String[] args) {
JFrame.setDefaultLookAndFeelDecorated(true);
new ServerFrom();
}
}