模擬QQ聊天
一、要求
1、一個服務器可以與多個用戶同時通訊
2、用戶可以通過服務器與用戶之間通訊
3、用戶可以選擇和所有人發消息,也可以選擇和某個用戶單獨發消息
4、服務器要顯示當前所有在線人員
5、用戶要顯示當前在線的人員
6、當有新用戶登錄時或在線用戶退出時,服務器要向所有其他在線用戶發送提示信息,並且服務器也要顯示相應的提示信息
7、不能有相同的用戶名同時登陸
8、不能發送空消息
9、客戶端可以設置連接的服務器IP和端口
二、瞭解B/S模式的底層socket通訊原理
QQ聊天可以利用協議方式發送消息。所以先要了解瀏覽器和服務器直接的協議,從而仿照。
瀏覽器的請求
GET / HTTP/1.1
Accept: text/html, application/xhtml+xml, */*
Accept-Language: zh-CN
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
Accept-Encoding: gzip, deflate
Host: 192.168.31.169:9090
DNT: 1
Connection: Keep-Alive
請求行,包含: 請求方式(GET或POST) 空格 請求的資源路徑 空格 http的協議版本
接下來是請求消息頭(...)
空行
請求體(包括瀏覽器向服務器提交的表單數據等)
HTTP/1.1 200 OK
Date: Fri, 11 Sep 2015 12:33:43 GMT
Server: Apache-Coyote/1.1
Content-Type: text/html;charset=UTF-8
Set-Cookie: JSESSIONID=409C8CF8220AD78D26D47B15DCEADCD3; Path=/; HttpOnly
Vary: Accept-Encoding
Connection: close
Transfer-Encoding: chunked
Content-Language: zh-CN
應答行,包含:http協議版本 空格 應答狀態碼 空格 應答狀態碼信息碼描述
應答消息頭(...)
空行
應答體(頁面內容)
三、QQ聊天協議
在服務器端 用一個HashMap<userName,socket> 維護所有用戶相關的信息,從而能夠保證和所有的用戶進行通訊。
客戶端的動作:
(1)連接(登錄):發送userName 服務器的對應動作:1)界面顯示,2)通知其他用戶關於你登錄的信息, 3)把其他在線用戶的userName通知當前用戶 4)開啓一個線程專門爲當前線程
服務
(2)退出(註銷):
(3)發送消息
※※發送通訊內容之後,對方如何知道是幹什麼,通過消息協議來實現:
客戶端向服務器發的消息格式設計:
命令關鍵字@#接收方@#消息內容@#發送方
連接:userName ----握手的線程serverSocket專門接收該消息,其它的由服務器新開的與客戶進行通訊的socket來接收
退出:exit@#全部@#null@#userName
發送: on @# JList.getSelectedValue() @# tfdMsg.getText() @# tfdUserName.getText()
服務器向客戶端發的消息格式設計:
命令關鍵字@#發送方@#消息內容
登錄:
1) msg @#server @# 用戶[userName]登錄了 (給客戶端顯示用的)
2) cmdAdd@#server @# userName (給客戶端維護在線用戶列表用的)
退出:
1) msg @#server @# 用戶[userName]退出了 (給客戶端顯示用的)
2) cmdRed@#server @# userName (給客戶端維護在線用戶列表用的)
發送:
msg @#消息發送者( msgs[3] ) @# 消息內容 (msgs[2])
四、註解和實現代碼
ClientForm.java類
package com.sina.chat;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
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 ClientForm extends JFrame implements ActionListener{
private JTextField userName;//用戶名
private DefaultListModel lm;//用於維護在線用戶列表
private JList list;
private JTextField msg;//發送消息口
private JTextArea allMsg;
private JButton btnConn;
private String HOST="127.0.0.1";//服務器地址
private int PORT = 9090;//服務器端口號
public ClientForm(){
addJMnuBar();//添加並處理自定義菜單
JPanel upP = new JPanel();
upP.add(new JLabel("用戶標識"));
userName = new JTextField(10);
upP.add(userName);
//上部面板
btnConn = new JButton("連接");
btnConn.setActionCommand("conn");
btnConn.addActionListener(this);
upP.add(btnConn);
JButton btnExit = new JButton("退出");
btnExit.setActionCommand("exit");
btnExit.addActionListener(this);
upP.add(btnExit);
this.getContentPane().add(upP, BorderLayout.NORTH);
//中部面板
JPanel cenP = new JPanel(new BorderLayout());
//以下這段設置“在線用戶”列表 ----東
lm = new DefaultListModel();//如果版本不對,可以添加<String>
list = new JList(lm);
lm.addElement("全部");
list.setSelectedIndex(0);
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);//設置只能進行單項選擇
list.setVisibleRowCount(2);//設置默認顯示的行數
JScrollPane jc = new JScrollPane(list);
jc.setBorder(new TitledBorder("在線"));
//setSize是設定的固定大小,而setPreferredSize僅僅是設置最好的大小,
//這個不一定與實際顯示出來的控件大小一致(根據界面整體的變化而變化)
jc.setPreferredSize(new Dimension(70,cenP.getHeight()));
cenP.add(jc,BorderLayout.EAST);
//以下這段設置消息發送面板 ----南
JPanel downP = new JPanel();
downP.add(new JLabel("消息"));
msg = new JTextField(20);
downP.add(msg);
JButton btnSend = new JButton("發送");
btnSend.setActionCommand("send");
btnSend.addActionListener(this);
downP.add(btnSend);
cenP.add(downP,BorderLayout.SOUTH);
//以下設置中間的聊天記錄
allMsg = new JTextArea();
allMsg.setEditable(false);
allMsg.setLineWrap(true);//設置文本域自動換行
allMsg.setWrapStyleWord(true);
// cenP.add(new JScrollPane(allMsg));//把消息框用滾動面板包起來,再加到center中
cenP.add(allMsg);
this.getContentPane().add(cenP,BorderLayout.CENTER);
this.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e){
sendExitMsg();//發送退出信息
}
});
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//設置框架上的關閉按鈕
this.setBounds(300, 300, 400, 300);
this.setVisible(true);
}
private void addJMnuBar() {
JMenuBar bar = new JMenuBar();
JMenu m=new JMenu("選項");
JMenuItem mi = new JMenuItem("設置");
mi.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
//因爲setDlg、host等變量要放在內部類中,所以要設置成final型
final JDialog setDlg = new JDialog(ClientForm.this,true);
setDlg.setBounds(ClientForm.this.getX()+40, ClientForm.this.getY()+100, 350, 100);
setDlg.setLayout(new FlowLayout());
JButton btn = new JButton("設置");
final JTextField host = new JTextField(10);
host.setText("127.0.0.1");
final JTextField port = new JTextField(5);
port.setText("9090");
btn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
ClientForm.this.HOST = host.getText();
try {
ClientForm.this.PORT = Integer.parseInt(port.getText());
} catch (NumberFormatException e1) {
JOptionPane.showMessageDialog(ClientForm.this,"請輸入數字");
}
setDlg.dispose();
}
});
setDlg.add(new JLabel("服務器IP:"));
setDlg.add(host);
setDlg.add(new JLabel("端口號:"));
setDlg.add(port);
setDlg.add(btn);
setDlg.setVisible(true);
}
});
JMenuItem help = new JMenuItem("幫助");
help.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JDialog dlg = new JDialog(ClientForm.this,true);
dlg.setBounds(ClientForm.this.getX()+40, ClientForm.this.getY()+100, 300, 80);
dlg.setLayout(new FlowLayout());
JLabel label = new JLabel("版本所有@城院.2015-9-13,QQ:888888");
dlg.add(label);
dlg.setVisible(true);
}
});
bar.add(m);
m.add(mi);
m.add(help);
this.setJMenuBar(bar);
}
public static void main(String[] args){
JFrame.setDefaultLookAndFeelDecorated(true);
new ClientForm();
}
public void actionPerformed(ActionEvent e) {
//連接按鈕
if(e.getActionCommand().equals("conn")){
System.out.println("連接。。。");
connercting();//與服務器建立連接
}else if(e.getActionCommand().equals("send")){
sendMsg();//發送聊天消息
}else if(e.getActionCommand().equals("exit")){
sendExitMsg();//發送退出消息
}
}
private Socket client;//聲明一個客戶端的套接字
private PrintWriter pw;//聲明一個打印流
private void connercting() {
try {
//握手
client = new Socket(HOST,PORT);
String userName = this.userName.getText();
if(userName.equals("")){//控制用戶名不能爲空
JOptionPane.showMessageDialog(this, "用戶名不能爲空");
return ;
}
//連接成功後,將用戶名框和連接按鈕設置爲不可選
btnConn.setEnabled(false);
this.userName.setEditable(false);
pw=new PrintWriter(client.getOutputStream(), true);
pw.println(userName);
this.setTitle("用戶【"+userName+"】在線");
new ClientThread().start();//在線聊天處理
} catch (IOException e) {
JOptionPane.showMessageDialog(null, "服務器連接失敗!!!");
}
}
//發送退出消息
private void sendExitMsg() {
if(client==null){
System.exit(0);
}
//自定義消息協議
String msg = "exit@#"+"全部"+"@#"+null+"@#"+userName.getText();
pw.println(msg);
pw.flush();//這裏一定要記得刷新
System.exit(0);
}
//發送聊天消息
private void sendMsg() {
String strMsg=this.msg.getText();
if(strMsg.equals("")){//保證發送的消息不能爲空
JOptionPane.showMessageDialog(this, "不能發送空消息。。。");
return ;
}
String msg="on@#"+list.getSelectedValue()+"@#"+strMsg+"@#"+userName.getText();
pw.println(msg);
pw.flush();
this.msg.setText("");
}
class ClientThread extends Thread{
public void run(){
try {
Scanner sc = new Scanner(client.getInputStream());
while(sc.hasNextLine()){
String str = sc.nextLine();
String[] msgs=str.split("@#");
//通過聊天協議,解析服務端發送來的消息
if("msg".equals(msgs[0])){
if("server".equals(msgs[1])){
if("error".equals(msgs[2])){//判斷連接到服務器的客戶中是否有同名
btnConn.setEnabled(true);
userName.setEditable(true);
JOptionPane.showMessageDialog(ClientForm.this, "用戶名被佔用");
continue;
}else{
str="【通知】:"+msgs[2];
}
}else{
str="【"+msgs[1]+"】說:"+msgs[2];
}
allMsg.append("\r\n"+str);
}else if("cmdAdd".equals(msgs[0])){
System.out.println(111111111);
lm.addElement(msgs[2]);
}else if("cmdRed".equals(msgs[0])){
System.out.println(6666);
lm.removeElement(msgs[2]);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
ServerForm.java類
package com.sina.chat;
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 ServerForm extends JFrame {
private int PORT = 9090;
private DefaultListModel lm;//用於維護在線用戶列表
private JList list;
private JTextArea area;
private Map<String,Socket> usersMap = new HashMap<String,Socket>();
public ServerForm(){
super("這是服務器");
//聊天消息記錄框
area = new JTextArea();
area.setEditable(false);
area.setLineWrap(true);
this.getContentPane().add(new JScrollPane(area));//默認加在center位置
//界面右邊的在線用戶列表
lm = new DefaultListModel();
list = new JList(lm);
JScrollPane jc = new JScrollPane(list);
jc.setBorder(new TitledBorder("在線"));
//setSize是設定的固定大小,而setPreferredSize僅僅是設置最好的大小,
//這個不一定與實際顯示出來的控件大小一致(根據界面整體的變化而變化)
jc.setPreferredSize(new Dimension(100,this.getHeight()));
this.getContentPane().add(jc,BorderLayout.EAST);
//菜單
JMenuBar bar =new JMenuBar();
this.setJMenuBar(bar);
JMenu m=new JMenu("控制");
m.setMnemonic('C');
bar.add(m);
//“開啓”菜單項,因爲內部類要用到這個變量,所以定義爲final型
final JMenuItem run = new JMenuItem("開啓");
run.setAccelerator(KeyStroke.getKeyStroke('R',KeyEvent.CTRL_MASK));
run.setActionCommand("run");
m.add(run);
m.addSeparator(); //菜單分隔線
//“開啓”菜單項
JMenuItem exit = new JMenuItem("退出");
exit.setAccelerator(KeyStroke.getKeyStroke('E',KeyEvent.CTRL_MASK));//設置快捷鍵
exit.setActionCommand("exit");
m.add(exit);
ActionListener a = new ActionListener() {
public void actionPerformed(ActionEvent e) {
if(e.getActionCommand().equals("run")){
startServer();
run.setEnabled(false);//點擊運行按鈕後,將運行按鈕設置爲不可選
}else if(e.getActionCommand().equals("exit")){
System.exit(0);
}
}
};
run.addActionListener(a);
exit.addActionListener(a);
//整個界面窗口的設置
final int winWidth = 500;
final int winHeight = 400;
Toolkit toolkit = Toolkit.getDefaultToolkit();
int width = (int) toolkit.getScreenSize().getWidth();//獲得系統分辨率
int height = (int) toolkit.getScreenSize().getHeight();
this.setBounds(width/2-winWidth/2, height/2-winHeight/2, winWidth, winHeight);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setVisible(true);
}
public static void main(String[] args) {
JFrame.setDefaultLookAndFeelDecorated(true);
new ServerForm();
}
private void startServer() {
try {
ServerSocket server = new ServerSocket(PORT);
area.append("啓動服務器:\r\nIP:"+server.getInetAddress().getHostAddress()+"\r\nPORT:"+server.getLocalPort());
new ServerThread(server).start();
} catch (IOException e) {
e.printStackTrace();
}
}
class ServerThread extends Thread{
ServerSocket server = null;
public ServerThread(ServerSocket server) {
this.server = server;
}
//專門處理握手消息
public void run(){
try {
while(true){
Socket socketClient = server.accept();
Scanner sc = new Scanner(socketClient.getInputStream());
if(sc.hasNextLine()){
String userName =sc.nextLine();
if(isError(userName)){//判斷用戶名是否存在,若存在則給用戶發送提示信息
PrintWriter pw = new PrintWriter(socketClient.getOutputStream(),true);
String msg = "msg@#server@#error";
pw.println(msg);
pw.flush();
continue;
}
area.append("\r\n用戶:【"+userName+"】登錄,"+socketClient);
//把用戶添加到list當中----通過該list模塊的數據層控件lm來完成
lm.addElement(userName);
//1通知所有已經在線的人,userName這個人登錄了
msgAll(userName);
//2告訴userName這個人,有哪些人目前在線
msgSelf(socketClient);
//3開一個專門用於和該userName客戶端通訊的線程
new ClientThread(socketClient);
//4把該用戶放到“在線用戶池”中
usersMap.put(userName, socketClient);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
//判斷用戶名是否已經存在了
private boolean isError(String userName) {
return usersMap.containsKey(userName);
}
//通知所有已經在線的人,userName這個人登錄了
private void msgAll(String userName) {
Iterator<Socket> it = usersMap.values().iterator();
while(it.hasNext()){
Socket s = it.next();
try {
PrintWriter pw = new PrintWriter(s.getOutputStream(),true);
//給用戶顯示在消息框中的
String msg = "msg@#server@#用戶【"+userName+"】登錄了";
pw.println(msg);
pw.flush();//刷新、刷新、刷新
//給用戶維護JList列表(在線用戶)
msg = "cmdAdd@#server@#"+userName;
pw.println(msg);
pw.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//告訴userName這個人,有哪些人目前在線.只要給客戶端發送“維護JList列表(在線用戶)的消息”
private void msgSelf(Socket socketClient) {
PrintWriter pw = null;
try {
pw = new PrintWriter(socketClient.getOutputStream(),true);
Iterator<String> it = usersMap.keySet().iterator();
while(it.hasNext()){
String userName = it.next();
String msg = "cmdAdd@#server@#"+userName;
pw.println(msg);
}
} catch (IOException e) {
e.printStackTrace();
}
pw.flush();
}
}
class ClientThread extends Thread{
private Socket client;
public ClientThread(Socket client) {
this.client = client;
start();
}
public void run(){
try {
Scanner sc = new Scanner(client.getInputStream());
while(sc.hasNext()){
String str = sc.nextLine();
String[] msgs = str.split("@#");
if("on".equals(msgs[0])){
sendMsgToSb(msgs);
}else if("exit".equals(msgs[0])){
//從“在線用戶池usersMap”中刪除當前用戶
usersMap.remove(msgs[3]);
//從界面中的“在線用戶列表”中刪除當前用戶,還要在服務器的area中顯示該用戶的退出消息
lm.removeElement(msgs[3]);
area.append("\r\n用戶【"+msgs[3]+"】退出了");
//發該用戶退出的消息發給其他在線用戶
sendExitMsgToAll(msgs);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void sendMsgToSb(String[] msgs) throws IOException {
if("全部".equals(msgs[1])){
Iterator<Socket> it = usersMap.values().iterator();
while(it.hasNext()){
Socket s = it.next();
String str = "msg@#"+msgs[3]+"@#"+msgs[2];
PrintWriter pw = new PrintWriter(s.getOutputStream(),true);
pw.println(str);
pw.flush();
}
}else{
Socket s = usersMap.get(msgs[1]);
String str = "msg@#"+msgs[3]+"@#"+msgs[2];
PrintWriter pw = new PrintWriter(s.getOutputStream(),true);
pw.println(str);
pw.flush();
}
}
private void sendExitMsgToAll(String[] msgs) throws IOException {
Iterator<Socket> it = usersMap.values().iterator();
while(it.hasNext()){
Socket s =it.next();
//給用戶顯示在消息框中的
String str = "msg@#server@#【"+msgs[3]+"】退出了";
PrintWriter pw = new PrintWriter(s.getOutputStream(),true);
pw.println(str);
pw.flush();
//給用戶維護JList列表(在線用戶)
str = "cmdRed@#server@#"+msgs[3];
pw.println(str);
pw.flush();
}
}
}
}
五、知識點清單
1、java圖形界面基礎知識(javax.swing和java.awt)
2、setPreferredSize:(1)setPreferredSize需要在使用佈局管理器的時候使用,佈局管理器會獲取空間的preferredsize,因而可以生效。例如borderlayout在north中放入一個panel,panel的
高度可以通過這樣實現:panel.setPreferredSize(new Dimension(0, 100));這樣就設置了一個高度爲100的panel,寬度隨窗口變化。
(2)setSize,setLocation,setBounds方法需要在不使用佈局管理器的時候使用,也就是setLayout(null)的時候可以使用這三個方法控制佈局。
區分好這兩個不同點之後,我相信你的佈局會更隨心所欲。
3、DefaultListModel:創建並且設置列表數據模型,和JList配合使用,使添加刪除JList上的元素時變得容易
4、setLineWrap:public void setLineWrap(boolean wrap)設置文本區的換行策略。
如果設置爲 true,則當行的長度大於所分配的寬度時,將換行。
如果設置爲 false,則始終不換行。當策略更改時,將激發 PropertyChange 事件("lineWrap")。此屬性默認爲 false
public void setWrapStyleWord(boolean word)設置換行方式(如果文本區要換行)。
如果設置爲 true,則當行的長度大於所分配的寬度時,將在單詞邊界(空白)處換行。
如果設置爲 false,則將在字符邊界處換行。此屬性默認爲 false。
5、匿名類
6、內部類
7、網絡編程技術
六、運行結果截圖