(JAVA)使用swing組件模仿QQ界面+網絡編程實現QQ消息傳輸


直接貼當時的實驗報告吧。

1、課程題目

模仿騰訊QQ實現一個即時聊天軟件,可以進行好友管理以及私聊等功能。

1.1功能性分類

功能類別

功能名稱、標識符

描述

 

 

用戶信息

用戶登陸

用戶登陸

用戶注冊

用戶注冊

修改密碼

通過驗證后修改密碼

修改信息

修改昵稱,個性簽名,生日,學校,公司,職業,所在地

修改狀態

修改登錄狀態,包括在線,忙碌,離開

用戶交互

添加好友

添加好友

私聊

一對一聊天

雲端存儲

存儲聊天記錄

存儲聊天記錄

 

 

 

 

 

 

 

 

 

 

 

 

 

 

2、題目分析與設計

2.1使用的開發環境

我使用的開發環境是Eclipse,jdk8.0,數據庫使用Mysql。

2.2軟件功能架構圖

 

 

3、功能實現設計

3.1系統總體結構

3.2登陸功能實現

 

3.3注冊功能實現

 

 

 

3.4修改信息/狀態實現

 

 

3.5修改密碼實現

 

 

3.6發送消息實現

 

 

3.7添加好友實現

 

 

4、數據庫設計

4.1用戶(user)表

 

 

序號

字段名

數據類型

長度

主鍵

允許空

默認值

說明

1

id

int

32

 

用戶編號

2

accout

char

32

 

 

用戶郵箱賬號

3

pasword

char

32

 

 

用戶密碼

4

nickname

char

64

 

 

用戶昵稱

5

autograph

char

128

 

 

用戶個性簽名

6

gender

tinyint

1

 

 

用戶性別

7

birthday

date

-

 

 

用戶生日

8

location

char

64

 

 

用戶所在地

9

school

char

64

 

 

用戶學校

10

company

char

64

 

 

用戶公司

11

job

char

64

 

 

用戶職業

12

   statu

int

4

 

 

用戶狀態

13

create_time

datetime

-

 

 

用戶創建時間

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

4.2好友列表(list)表

 

 

序號

字段名

數據類型

長度

主鍵

允許空

默認值

說明

1

id

int

32

 

好友編號

2

accout

int

32

 

 

好友的id號

 

 

 

 

4.3聊天記錄(chat)表

 

序號

字段名

數據類型

長度

主鍵

允許空

默認值

說明

1

sender_id

int

32

 

發送者的id

2

content

varchar

256

 

 

消息內容

3

send_time

datetime

-

 

 

發送消息時間

 

 

 

 

 

5、具體實現

由於代碼太長,大約有6500行,全部貼出來會占很大一部分空間,所以這里只講一下很關鍵也很重要,並且有一點難點的實現。

5.1登錄界面

現在正值移動互聯網時代,人們在關注功能的同時,也加強了對於視覺體驗的追求。作為一款即時聊天軟件自然需要很高的顏值,但是由於java自帶的GUI顏值有限,只能自己通過swing組件完成比較漂亮的界面。

由於騰訊QQ的界面較漂亮,這里略微的模仿了他的界面,但由於關於swing組件的文檔資料並不多,這里的實現變得十分艱辛,但經過不懈努力,仍然較完美的完成了界面的設計和實現。

5.1.1文本框/密碼框

在騰訊QQ的登錄界面中,文本框和密碼框表現的效果比較豐富,比如說圓角矩形邊框,鼠標移動上去框會變色等等等等。這里同樣實現了這樣的功能。

 1 public class LoginRoundTextBox extends JTextField {
 2     Color bordercolor = UColor.InputDefaultBorderColor;
 3     boolean Cover = false;
 4     public void BorderHigh() { 
 5         bordercolor = UColor.InputCoverBorderColor;
 6         Cover = true;
 7         this.repaint();
 8     }
 9     public void BorderLow() {
10         bordercolor = UColor.InputDefaultBorderColor;
11         Cover = false;
12         this.repaint();
13     }
14     protected void paintBorder(Graphics g) {
15         int h = getHeight();// 從JComponent類獲取高寬
16         int w = getWidth();
17         Graphics2D g2d = (Graphics2D) g.create();
18         Shape shape = g2d.getClip();
19         g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
20         g2d.setClip(shape);
21         g2d.setColor(bordercolor);
22         if (Cover) {
23             g2d.setStroke(new BasicStroke(1.8f));
24         } else {
25             g2d.setStroke(new BasicStroke(1.0f));
26         }
27         g2d.drawRoundRect(0, 0, w - 1, h - 1, 5, 5);
28         g2d.dispose();
29         super.paintBorder(g2d);
30     }
31 }

密碼框的實現類似,只是需要繼承JPasswordField。

5.1.2登錄按鈕

同樣,在騰訊QQ登錄界面中,登錄按鈕也具有不錯的效果。這里也實現了差不多的效果。

 1 public class LoginRoundButton extends JButton {
 2     Color bgcolor = UColor.ButtonDefaultColor;
 3     Boolean Cover = false;
 4     public LoginRoundButton(String ins) {
 5         super(ins);
 6     }
 7     public void BgHigh() {
 8         bgcolor = UColor.ButtonCoverColor;
 9         this.repaint();
10     }
11     public void BgLow() {
12         bgcolor = UColor.ButtonDefaultColor;
13         this.repaint();
14     }
15     protected void paintBorder(Graphics g) {
16         int h = getHeight();// 從JComponent類獲取高寬
17         int w = getWidth();
18         Graphics2D g2d = (Graphics2D) g.create();
19         Shape shape = g2d.getClip();
20         g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
21         g2d.setClip(shape);
22         g2d.setStroke(new BasicStroke(7f));
23         g2d.setColor(UColor.OperatorBackgroundColor);
24         g2d.drawRoundRect(0, 0, w - 1, h - 1, 15, 15);
25         g2d.dispose();
26         this.setBackground(bgcolor);
27         super.paintBorder(g2d);
28     }
29 }

5.1.3頭像以及狀態選擇按鈕

左側顯示用戶頭像,右下角點擊后會有下拉菜單跳出設置登錄的狀態,盡管不是很完美,但這里仍然粗略的實現了。在主面板將頭像所在區域作為一個新的jpanel,而頭像作為image直接畫進jpanel中直接作為它的背景。考慮到實現,狀態選擇按鈕利用了JMenuBar的特性,實現了這一功能,將其仍然放在主面板中,由於登錄界面的長寬是固定不可變的,使用null布局可以讓這些組件很好的定位。這是頭像的實現:

 1 public class LoginHeadPanel extends JPanel {
 2     Image image = null;
 3     public LoginHeadPanel() {
 4         this.setBounds(40, 10, 85, 85);
 5         image = new ImageIcon(UImport.UserHeadPicture).getImage();
 6     }
 7     protected void paintComponent(Graphics g) {
 8         int x = image.getWidth(this);
 9         int y = image.getHeight(this);
10         g.drawImage(image, 0, 0, x, y, this);
11     }
12 }

這是狀態選擇按鈕的實現:

1 public class LoginStatusCelectButton extends JMenu {
2     public LoginStatusCelectButton() {
3         this.setBounds(65, 65, 29, 29);
4         this.setBorder(null);
5         this.setForeground(null);
6         this.setBackground(null);
7         this.setContentAreaFilled(false);
8     }
9 }
 1 StatusBar = new JMenuBar();
 2 StatusBar.setBorder(null);
 3 ImageIcon statuico = new ImageIcon(UImport.UserStatuOnline);
 4 LoginStatusCelectButton statubutton = new LoginStatusCelectButton();
 5 statubutton.setIcon(statuico);
 6 StatusBar.add(statubutton);
 7 StatusBar.setBounds(100, 70, 15, 15);
 8 StatusBar.setSize(new Dimension(20, 20));
 9 StatusBar.setBackground(null);
10 statubutton.setBackground(null);
11 JMenuItem Online = new JMenuItem("在線");
12 JMenuItem Away = new JMenuItem("離開");
13 JMenuItem Busy = new JMenuItem("忙碌");
14 ImageIcon onlineicon = new ImageIcon(UImport.UserStatuOnline);
15 ImageIcon Awayicon = new ImageIcon(UImport.UserStatuAway);
16 ImageIcon Busyicon = new ImageIcon(UImport.UserStatuBusy);
17 Online.addActionListener(new ActionListener() {
18     public void actionPerformed(ActionEvent e) {
19         userstatu = User.ONLINE;
20         statubutton.setIcon(onlineicon);
21     }    });
22 Away.addActionListener(new ActionListener() {
23     public void actionPerformed(ActionEvent e) {
24         userstatu = User.AWAY;
25                 statubutton.setIcon(Awayicon);
26     }
27 });
28 Busy.addActionListener(new ActionListener() {
29     public void actionPerformed(ActionEvent e) {
30         userstatu = User.BUSY;
31         statubutton.setIcon(Busyicon);
32     }
33 });
34 Online.setFont(font);
35 Away.setFont(font);
36 Busy.setFont(font);
37 Online.setIcon(onlineicon);
38 Away.setIcon(Awayicon);
39 Busy.setIcon(Busyicon);
40 statubutton.add(Online);
41 statubutton.add(Away);
42 statubutton.add(Busy);

此處可以將JMenubar進行再次封裝,使這這里和主界面中的狀態選擇欄無需同樣的代碼,。由於時間原因,又比較簡答,不再實現。

5.2注冊界面

5.2.1文本框合法檢查以及提示與警告

由於大部分軟件的注冊一般都是在網頁中進行注冊,這里再次模仿了網頁中的設計,在文本框失焦時,進行合法判定並給出一定的提示或警告。

郵箱賬號的合法檢查使用正則表達式匹配,方便快捷,除了檢查是否是合法郵箱外還需要檢查是否為空:

 1 public void setAccount() {
 2         // 郵箱正則
 3 String check=
 4 "^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$";
 5         Pattern regex = Pattern.compile(check);
 6         emaillabel = new JLabel(UMap.RegisterAccount);
 7         emaillabel.setBounds(101, 42, 50, 30);
 8         emaillabel.setFont(labelfont);
 9         this.add(emaillabel);
10         emailnotelabel = new JLabel();
11         emailnotelabel.setBounds(450, 42, 250, 30);
12         emailnotelabel.setFont(notefont);
13         emailnotelabel.setForeground(UColor.RegisterNoteWordColor);
14         this.add(emailnotelabel);
15         emailbox = new RegisterInformationBox();
16         emailbox.addFocusListener(new FocusListener() {
17             public void focusLost(FocusEvent e) {
18                 if (emailbox.getText().equals("")) {
19                     emailbox.BorderWarning();
20                     emailnotelabel.setText(UMap.RegisterAccountWarning);
21                     emailnotelabel.setIcon(null);
22                     emailnotelabel.setForeground(UColor.RegisterWarningWordColor);
23                 } else if (!regex.matcher(emailbox.getText()).matches()) {
24                     emailbox.BorderWarning();
25                     emailnotelabel.setText(UMap.RegisterAccountNote);
26                     emailnotelabel.setIcon(null);
27                     emailnotelabel.setForeground(UColor.RegisterWarningWordColor);
28                 } else {
29                     emailbox.BorderOK();
30                     emailnotelabel.setIcon(OK);
31                     emailnotelabel.setText(null);
32                 }
33             }
34 
35             public void focusGained(FocusEvent e) {
36                 emailbox.BorderHigh();
37                 emailnotelabel.setText(UMap.RegisterAccountNote);
38                 emailnotelabel.setIcon(null);
39                 emailnotelabel.setForeground(UColor.RegisterNoteWordColor);
40             }
41 
42         });
43         emailbox.setBounds(160, 42, 280, 35);
44         emailbox.setFont(inputfont);
45         this.add(emailbox);
46     }

密碼框是這個界面的重中之重,需要小心設計,更要做足檢查,這里參考騰訊QQ注冊賬號時設置密碼的要求(長度為6-16個字符,不能包含空格,不能是9位以下的純數字),實現了合法檢查。

 1 passwordbox.addFocusListener(new FocusListener() {
 2             public void focusLost(FocusEvent e) {
 3                 String temp = new String(passwordbox.getPassword());
 4                 System.out.println(temp);
 5                 if (temp.length() == 0) {
 6                     passwordbox.BorderWarning();
 7                     passwordnotelabel.setText(UMap.RegisterPasswordWarning);
 8                     passwordnotelabel.setForeground(UColor.RegisterWarningWordColor);
 9                 } else if (temp.length() < 6 || temp.length() > 16) {
10                     passwordbox.BorderWarning();
11                     passwordnotelabel.setText(UMap.RegisterPasswordNote);
12                     passwordnotelabel.setForeground(UColor.RegisterWarningWordColor);
13                 } else {
14                     boolean havenull = false;
15                     boolean alldigit = true;
16                     for (int i = 0; i < temp.length(); i++) {
17                         if (temp.charAt(i) == ' ') {
18                             havenull = true;
19                             break;
20                         }
21                         if (temp.charAt(i) < '0' || temp.charAt(i) > '9') {
22                             alldigit = false;
23                         }
24                     }
25                     if (havenull || (alldigit && temp.length() < 9)) {
26                         passwordbox.BorderWarning();
27                         passwordnotelabel.setText(UMap.RegisterPasswordNote);
28                         passwordnotelabel.setForeground(UColor.RegisterWarningWordColor);
29                     } else {
30                         passwordbox.BorderOK();
31                         passwordnotelabel.setIcon(OK);
32                         passwordnotelabel.setText(null);
33                     }
34                 }
35             }
36             public void focusGained(FocusEvent e) {
37                 passwordbox.BorderHigh();
38                 passwordnotelabel.setText(UMap.RegisterPasswordNote);
39                 passwordnotelabel.setIcon(null);
40                 passwordnotelabel.setForeground(UColor.RegisterNoteWordColor);
41             }
42         });

其他的檢查如果沒有特殊要求,均判斷是否為空即可。

5.2.2生日選擇器

很顯然這里需要3個下拉選擇框,但是比較復雜的是需要特殊處理閏年,還有超過現在的時間。而且。對於JComboBox的監聽,我們需要用到PopupMenuListener,通過查閱資料最終實現了這個簡單的功能。

 1  yearbox.addPopupMenuListener(new PopupMenuListener() {
 2             public void popupMenuCanceled(PopupMenuEvent e) {
 3             }
 4             public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
 5                 String choose = (String) yearbox.getSelectedItem();
 6                 int yy = Integer.parseInt(choose.substring(0, choose.length() - 1));
 7                 if ((yy % 100 != 0 && yy % 4 == 0) || yy % 400 == 0) {
 8                     isleap = true;
 9                 } else {
10                     isleap = false;
11                 }
12                 monthbox.removeAllItems();
13                 int last;
14                 if (yy == lastestyear) {
15                     last = lastestmonth;
16                 } else {
17                     last = 12;
18                 }
19                 for (int i = 1; i <= last; i++) {
20                     monthbox.addItem(i + "月");
21                 }
22             }
23             public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
24             }
25         });
 1 monthbox.addPopupMenuListener(new PopupMenuListener() {
 2             public void popupMenuCanceled(PopupMenuEvent e) {
 3             }
 4             public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
 5                 String choose = (String) monthbox.getSelectedItem();
 6                 int mm = Integer.parseInt(choose.substring(0, choose.length() - 1));
 7                 int twoplus = 0;
 8                 if (isleap) {
 9                     twoplus = 1;
10                 }
11                 int last;
12                 if (mm == lastestmonth) {
13                     last = lastestday;
14                 } else if (mm == 2) {
15                     last = mtod[mm - 1] + twoplus;
16                 } else {
17                     last = mtod[mm - 1];
18                 }
19                 daybox.removeAllItems();
20                 for (int i = 1; i <= last; i++) {
21                     daybox.addItem(i + "日");
22                 }
23             }
24             public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
25             }
26         });

5.3用戶主界面

5.3.1標簽頁

用一個JTabbedPane分開兩個不同面板,就像騰訊QQ中聯系人,最近消息,群組不同的標簽,可以很方便的在同一個位置分開不同的面板。

 1 midpane = new JTabbedPane();
 2 midpane.setPreferredSize(new Dimension(402, 660));
 3 midpane.setBorder(null);
 4 midpane.setBackground(UColor.MainMidPanelTabgroundColor);
 5 midpane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
 6 midpane.addTab(null, peopledefaulticon, mainmidpeoplepanel, UMap.MainPeople);
 7 midpane.addTab(null, recentdefaulticon, mainmidrecentpanel, UMap.MainRecent);
 8 midpane.setEnabledAt(1, false);
 9 midpane.setSelectedIndex(0);
10 this.add(midpane, BorderLayout.CENTER);

5.3.2好友列表

我個人覺得這是整個前端中最難的,但是通過查閱資料,依然還是比較好的實現了這個界面,首先用JScrollPane打底,讓面板可以自動增加滾動條進行滾動。其次,讓JScrollPane用JTabel來顯示,這樣可以很方便的讓每一個好友更像表格一樣出現在列表中。

但是有個很大難點,怎么樣讓JTabel里每一個單元格顯示3個組件:頭像,昵稱,個性簽名。查閱了很多資料,並且深入內部感受了JTabel的繪制方式,想了一個很巧妙的方法來實現單元格多組件和鼠標覆蓋單元格高亮。首先給JTabel添加一個自己繼承出來的單元格渲染器TableCellRenderer,並且重寫父類的getTableCellRendererComponent方法,其次給表格添加監聽器監聽鼠標的移動,一旦更換了單元格的位置,就重繪整個表格。

給表格加上MouseMotionListener監聽器:

1 table.addMouseMotionListener(new MouseMotionAdapter() {
2             public void mouseMoved(MouseEvent e) {
3                 int r = table.rowAtPoint(e.getPoint());
4                 if (r != MainUserCellRender.cover_r) {
5                     MainUserCellRender.cover_r = r;
6                     table.repaint();
7                 }
8             }
9         });

單元格渲染器的繼承:

 1 class MainUserCellRender extends JPanel implements TableCellRenderer {
 2     static int sad = 0;
 3     static int cover_r = -1;
 4     static int select_r = -1;
 5     static Font usernamefont;
 6     static Font userautographfont;
 7     static {
 8         usernamefont = LoadFont.loadFont(UImport.DefaultFont, 18);
 9         userautographfont = LoadFont.loadFont(UImport.DefaultFont, 14);
10     }
11     JLabel username;
12     JLabel userautograph;
13     public MainUserCellRender() {
14         super();
15         this.setLayout(null);
16         ImageIcon image = new ImageIcon(UImport.MainUserPicture);
17         JLabel userpic = new JLabel(image);
18         userpic.setBounds(10, 10, 50, 50);
19         username = new JLabel();
20         userautograph = new JLabel();
21         username.setBounds(68, 12, 200, 20);
22         username.setFont(usernamefont);
23         userautograph.setBounds(68, 36, 250, 20);
24         userautograph.setForeground(UColor.MainMidPanelUserLabelAutographColor);
25         userautograph.setFont(userautographfont);
26         this.add(userpic);
27         this.add(username);
28         this.add(userautograph);
29         
30     }
31     public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
32             int row, int column) {
33         User user = (User) value;
34         username.setText(user.getnickname());
35         userautograph.setText(user.getautograph());
36         if (row == select_r) {
37             this.setBackground(UColor.MainMidPanelUserLabelSelectedBackgroundColor);
38         } else if (row == cover_r) {
39             this.setBackground(UColor.MainMidPanelUserLabelCoverBackgroundColor);
40         } else {
41             this.setBackground(UColor.MainMidPanelTabgroundColor);
42         }
43         return this;
44     }
45 }

5.4聊天界面

整個軟件的主旋律就在這里,所以必須做的細致再細致。這里采用了現在流行的氣泡技術。

原本並沒有類似方法實現這種創意,網上的資料也是少之又少,不全或者直接沒有。但是這里同樣達到了較好的實現。

5.4.1分割面板

使用JSplitPane分割兩個面板,並且可以通過拖動,改變兩個面板的大小。使其更加人性化。

1 JSplitPane jsplitpane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, historytextpane, edittextpane);
2         jsplitpane.setDividerLocation(550);
3         this.add(jsplitpane, BorderLayout.CENTER);

5.4.2氣泡

這個實現比較難,我這里的實現是使用JPanel作為JScrollPane的顯示。掃描發送者輸入的文本來計算所需要氣泡的大小。

講整個大面板橫向分成多個JPanel,根據氣泡的高度動態調整該Jpanel的高度。再在這個小的JPanel里根據發送者重新定位頭像的位置和氣泡的位置。設定一個最大寬度,遍歷輸入的所有信息,計算氣泡需要的寬度和高度,將他放進小的JPanel中。

小JPanel的實現:

  1 private class MessagePane extends JPanel {
  2         static final int maxWIDTH = 500;
  3         static final int initHEIGHT = 65;
  4         ImageIcon image = new ImageIcon(UImport.MainUserPicture);
  5         JLabel userpicture = new JLabel(image);
  6         ChatEditPanel contentcontain = new ChatEditPanel();
  7         int height = initHEIGHT;
  8         int realwidth;
  9         boolean self;
 10         public MessagePane(boolean isself, String content) {
 11             self = isself;
 12             this.setBackground(Color.black);
 13             this.setLayout(null);
 14             // this.setOpaque(false);
 15             contentcontain.setFont(UFont.font[22]);
 16             contentcontain.setOpaque(false);
 17             contentcontain.setEditable(false);
 18             SimpleAttributeSet set = new SimpleAttributeSet();
 19             Document doc = contentcontain.getStyledDocument();
 20             FontMetrics fm = contentcontain.getFontMetrics(contentcontain.getFont());// 得到JTextPane
 21             int cnt = 0; // 的當前字體尺寸
 22             int paneWidth = contentcontain.getWidth();// 面板的寬度
 23             int fontheight = fm.getHeight();
 24             int allmaxwidth = 0;
 25             try {
 26                 for (int i = 0; i < content.length(); ++i) {
 27                     if (content.charAt(i) == '\n' || (cnt += fm.charWidth(content.charAt(i))) >= maxWIDTH) {// 當屬出字符的寬度大於面板的寬度時換行,也就是達到JTextPane不會出現水平的滾動條
 28                         System.out.println(cnt);
 29                         allmaxwidth=Math.max(allmaxwidth, cnt);
 30                         cnt = 0;
 31                         doc.insertString(doc.getLength(), "\n", set);
 32                         height += fontheight;
 33                         continue;
 34                     }
 35                     doc.insertString(doc.getLength(), String.valueOf(content.charAt(i)), set);
 36                 }
 37                 if (height == initHEIGHT) {
 38                     realwidth = cnt + 5;
 39                 } else {
 40                     realwidth = allmaxwidth+5;
 41                 }
 42                 // 就是將JTextPane中的插入符的位置移動到文本的末端!
 43             } catch (BadLocationException e) {
 44                 // TODO Auto-generated catch block
 45                 e.printStackTrace();
 46             }
 47             this.setBounds(5, nowheight, 760, height);
 48             nowheight += height + 5;
 49             contentcontain.setDocument(doc);
 50             if (isself) {
 51                 userpicture.setBounds(695, 5, image.getIconHeight(), image.getIconWidth());
 52                 contentcontain.setBounds(760 - realwidth - image.getIconHeight() - 35, 20, realwidth + 5, height - 25);
 53             } else {
 54                 userpicture.setBounds(5, 5, image.getIconHeight(), image.getIconWidth());
 55                 contentcontain.setBounds(75, 20, realwidth + 5, height - 25);
 56             }
 57             this.add(userpicture);
 58             this.add(contentcontain);
 59         }
 60         public void paintComponent(Graphics g) {
 61             Graphics2D g2D = (Graphics2D) g;
 62             g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
 63             int xPoints[] = new int[3];
 64             int yPoints[] = new int[3];
 65             if (self) {
 66                 g2D.setColor(UColor.CharBubbleColor_blue);
 67                 g2D.fillRoundRect(760 - realwidth - image.getIconHeight() - 35 - 5, 20 - 5, realwidth + 5 + 5,
 68                         height - 25 + 5, 10, 10);
 69                 // 繪制圓角消息氣泡邊框
 70                 g2D.setColor(UColor.CharBubbleColor_blue);
 71                 g2D.drawRoundRect(760 - realwidth - image.getIconHeight() - 35 - 5, 20 - 5, realwidth + 5 + 5,
 72                         height - 25 + 5, 10, 10);
 73 
 74                 xPoints[0] = (760 - realwidth - image.getIconHeight() - 35 - 5) + (realwidth + 5 + 5);
 75                 yPoints[0] = 20;
 76                 xPoints[1] = xPoints[0] + 8;
 77                 yPoints[1] = 20;
 78                 xPoints[2] = xPoints[0];
 79                 yPoints[2] = 20 + 6;
 80                 g2D.setColor(UColor.CharBubbleColor_blue);
 81                 g2D.fillPolygon(xPoints, yPoints, 3);
 82                 g2D.setColor(UColor.CharBubbleColor_blue);
 83                 g2D.drawPolyline(xPoints, yPoints, 3);
 84                 g2D.setColor(UColor.CharBubbleColor_blue);
 85                 g2D.drawLine(xPoints[0], yPoints[0] + 1, xPoints[2], yPoints[2] - 1);
 86             } else {
 87                 g2D.setColor(UColor.CharBubbleColor_orange);
 88                 g2D.fillRoundRect(75 - 5, 20 - 5, realwidth + 5 + 5, height - 25 + 5, 10, 10);
 89                 // 繪制圓角消息氣泡邊框
 90                 g2D.setColor(UColor.CharBubbleColor_orange);
 91                 g2D.drawRoundRect(75 - 5, 20 - 5, realwidth + 5 + 5, height - 25 + 5, 10, 10);
 92 
 93                 xPoints[0] = 75 - 5;
 94                 yPoints[0] = 20;
 95                 xPoints[1] = xPoints[0] - 8;
 96                 yPoints[1] = 20;
 97                 xPoints[2] = xPoints[0];
 98                 yPoints[2] = 20 + 6;
 99 
100                 g2D.setColor(UColor.CharBubbleColor_orange);
101                 g2D.fillPolygon(xPoints, yPoints, 3);
102                 g2D.setColor(UColor.CharBubbleColor_orange);
103                 g2D.drawPolyline(xPoints, yPoints, 3);
104                 g2D.setColor(UColor.CharBubbleColor_orange);
105                 g2D.drawLine(xPoints[0], yPoints[0] + 1, xPoints[2], yPoints[2] - 1);
106             }
107 
108         }
109 
110     }
 1 doc.insertString(doc.getLength(), String.valueOf(content.charAt(i)), set);
 2                 }
 3                 if (height == initHEIGHT) {
 4                     realwidth = cnt + 5;
 5                 } else {
 6                     realwidth = allmaxwidth+5;
 7                 }
 8                 // 就是將JTextPane中的插入符的位置移動到文本的末端!
 9             } catch (BadLocationException e) {
10                 // TODO Auto-generated catch block
11                 e.printStackTrace();
12             }
13             this.setBounds(5, nowheight, 760, height);
14             nowheight += height + 5;
15             contentcontain.setDocument(doc);
16             if (isself) {
17                 userpicture.setBounds(695, 5, image.getIconHeight(), image.getIconWidth());
18                 contentcontain.setBounds(760 - realwidth - image.getIconHeight() - 35, 20, realwidth + 5, height - 25);
19             } else {
20                 userpicture.setBounds(5, 5, image.getIconHeight(), image.getIconWidth());
21                 contentcontain.setBounds(75, 20, realwidth + 5, height - 25);
22             }
23             this.add(userpicture);
24             this.add(contentcontain);
25         }
26         public void paintComponent(Graphics g) {
27             Graphics2D g2D = (Graphics2D) g;
28             g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
29             int xPoints[] = new int[3];
30             int yPoints[] = new int[3];
31             if (self) {
32                 g2D.setColor(UColor.CharBubbleColor_blue);
33                 g2D.fillRoundRect(760 - realwidth - image.getIconHeight() - 35 - 5, 20 - 5, realwidth + 5 + 5,
34                         height - 25 + 5, 10, 10);
35                 // 繪制圓角消息氣泡邊框
36                 g2D.setColor(UColor.CharBubbleColor_blue);
37                 g2D.drawRoundRect(760 - realwidth - image.getIconHeight() - 35 - 5, 20 - 5, realwidth + 5 + 5,
38                         height - 25 + 5, 10, 10);
39                 xPoints[0] = (760 - realwidth - image.getIconHeight() - 35 - 5) + (realwidth + 5 + 5);
40                 yPoints[0] = 20;
41                 xPoints[1] = xPoints[0] + 8;
42                 yPoints[1] = 20;
43                 xPoints[2] = xPoints[0];
44                 yPoints[2] = 20 + 6;
45                 g2D.setColor(UColor.CharBubbleColor_blue);
46                 g2D.fillPolygon(xPoints, yPoints, 3);
47                 g2D.setColor(UColor.CharBubbleColor_blue);
48                 g2D.drawPolyline(xPoints, yPoints, 3);
49                 g2D.setColor(UColor.CharBubbleColor_blue);
50                 g2D.drawLine(xPoints[0], yPoints[0] + 1, xPoints[2], yPoints[2] - 1);
51             } else {
52                 g2D.setColor(UColor.CharBubbleColor_orange);
53                 g2D.fillRoundRect(75 - 5, 20 - 5, realwidth + 5 + 5, height - 25 + 5, 10, 10);
54                 // 繪制圓角消息氣泡邊框
55                 g2D.setColor(UColor.CharBubbleColor_orange);
56                 g2D.drawRoundRect(75 - 5, 20 - 5, realwidth + 5 + 5, height - 25 + 5, 10, 10);
57 
58                 xPoints[0] = 75 - 5;
59                 yPoints[0] = 20;
60                 xPoints[1] = xPoints[0] - 8;
61                 yPoints[1] = 20;
62                 xPoints[2] = xPoints[0];
63                 yPoints[2] = 20 + 6;
64 
65                 g2D.setColor(UColor.CharBubbleColor_orange);
66                 g2D.fillPolygon(xPoints, yPoints, 3);
67                 g2D.setColor(UColor.CharBubbleColor_orange);
68                 g2D.drawPolyline(xPoints, yPoints, 3);
69                 g2D.setColor(UColor.CharBubbleColor_orange);
70                 g2D.drawLine(xPoints[0], yPoints[0] + 1, xPoints[2], yPoints[2] - 1);
71             }
72 
73         }
74 
75     }
 1 g2D.drawLine(xPoints[0], yPoints[0] + 1, xPoints[2], yPoints[2] - 1);
 2             } else {
 3                 g2D.setColor(UColor.CharBubbleColor_orange);
 4                 g2D.fillRoundRect(75 - 5, 20 - 5, realwidth + 5 + 5, height - 25 + 5, 10, 10);
 5                 // 繪制圓角消息氣泡邊框
 6                 g2D.setColor(UColor.CharBubbleColor_orange);
 7                 g2D.drawRoundRect(75 - 5, 20 - 5, realwidth + 5 + 5, height - 25 + 5, 10, 10);
 8                 xPoints[0] = 75 - 5;
 9                 yPoints[0] = 20;
10                 xPoints[1] = xPoints[0] - 8;
11                 yPoints[1] = 20;
12                 xPoints[2] = xPoints[0];
13                 yPoints[2] = 20 + 6;
14                 g2D.setColor(UColor.CharBubbleColor_orange);
15                 g2D.fillPolygon(xPoints, yPoints, 3);
16                 g2D.setColor(UColor.CharBubbleColor_orange);
17                 g2D.drawPolyline(xPoints, yPoints, 3);
18                 g2D.setColor(UColor.CharBubbleColor_orange);
19                 g2D.drawLine(xPoints[0], yPoints[0] + 1, xPoints[2], yPoints[2] - 1);
20             }
21         }
22     }

5.5公共界面

5.5.1窗體頭部

因為swing自帶的窗體實在是太丑,這里對每個窗體的頭部都重新用JPanel模擬實現了,用天藍色作為底色,用Photoshop自己做了關閉和最小化按鈕的圖標。讓他看上去更符合正能量。

以注冊界面的頭部面板為例的實現:

 1 public class RegisterHeadPanel extends JPanel {
 2     JFrame loginframe, registerframe;
 3     Font font;
 4     public RegisterHeadPanel(JFrame frame1, JFrame frame2) {
 5         font = LoadFont.loadFont(UImport.DefaultFont, 18);
 6         loginframe = frame1;
 7         registerframe = frame2;
 8         this.setLayout(null);
 9         this.setPreferredSize(new Dimension(registerframe.getWidth(), 29));
10         this.setBackground(UColor.RegisterHeadBackgroundColor);
11         ImageIcon exit1ico = new ImageIcon(UImport.WindowExit1);
12         ImageIcon exit2ico = new ImageIcon(UImport.WindowExit2);
13         ImageIcon min1ico = new ImageIcon(UImport.WindowMin1);
14         ImageIcon min2ico = new ImageIcon(UImport.WindowMin2);
15         JButton exit = new JButton();
16         JButton min = new JButton();
17         exit.setIcon(exit1ico);
18         exit.setBackground(null);
19         exit.setPreferredSize(new Dimension(29, 29));
20         exit.setBorder(null);
21         exit.setMargin(new Insets(0, 0, 0, 0));
22         exit.setBounds(869, 0, exit1ico.getIconWidth(), exit2ico.getIconHeight());
23         ;
24         exit.addMouseListener(new MouseAdapter() {
25             public void mouseEntered(MouseEvent e) {
26                 exit.setIcon(exit2ico);
27             }
28             public void mouseExited(MouseEvent e) {
29                 exit.setIcon(exit1ico);
30             }
31             public void mouseClicked(MouseEvent e) {
32                 registerframe.dispose();
33                 loginframe.setEnabled(true);
34                 loginframe.requestFocus();
35             }
36         });
37         min.setIcon(min1ico);
38         min.setBackground(null);
39         min.setPreferredSize(new Dimension(29, 29));
40         min.setBorder(null);
41         min.setBounds(840, 0, exit1ico.getIconWidth(), exit2ico.getIconHeight());
42         ;
43         min.addMouseListener(new MouseAdapter() {
44             public void mouseEntered(MouseEvent e) {
45                 min.setIcon(min2ico);
46             }
47             public void mouseExited(MouseEvent e) {
48                 min.setIcon(min1ico);
49             }
50         });
51         this.add(min, new Integer(Integer.MIN_VALUE));
52         this.add(exit, new Integer(Integer.MIN_VALUE));
53         JLabel title = new JLabel(UMap.Signin);
54         title.setBounds(5, 5, 100, 20);
55         title.setFont(font);
56         title.setBackground(null);
57         title.setForeground(Color.WHITE);
58         this.add(title);
59 
60     }
61 }

5.5.2拖動監聽器

因為自己重新制定了窗體頭部的面板,所以自然要自己實現拖動,這里直接給整個窗體添加了監聽器,實際上還可以只給頭部面板添加。效果非常好。

 1 public class MouseDragListener implements MouseInputListener {
 2     Point origin;
 3     // 鼠標拖拽想要移動的目標組件
 4     JFrame frame;
 5     public MouseDragListener(JFrame frame) {
 6         this.frame = frame;
 7         origin = new Point();
 8     }
 9     public void mouseClicked(MouseEvent e) {
10     }
11     public void mouseEntered(MouseEvent e) {
12     }
13     public void mouseExited(MouseEvent e) {
14         this.frame.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
15     }
16     public void mousePressed(MouseEvent e) {
17         origin.x = e.getX();
18         origin.y = e.getY();
19     }
20     public void mouseReleased(MouseEvent e) {
21     }
22     public void mouseDragged(MouseEvent e) {
23         Point p = this.frame.getLocation();
24         this.frame.setLocation(p.x + (e.getX() - origin.x), p.y + (e.getY() - origin.y));
25     }
26     public void mouseMoved(MouseEvent arg0) {
27     }
28 }

5.6數據設計

5.6.1用戶類

客戶端與服務端通用的用戶類,用來傳遞用戶的信息。

包含有用戶狀態的定義,獲取和設置屬性的方法,以及用戶的各種屬性:昵稱,簽名,郵箱賬號,性別,所在地,生日,年齡,學校,公司,職業,登錄狀態。

 1 public class User extends Object implements Serializable {
 2     private static final long serialVersionUID = 1L;
 3     static public int OFFLINE = 0;
 4     static public int ONLINE = 1;
 5     static public int AWAY = 2;
 6     static public int BUSY = 3;
 7     public User(String account) {
 8         Account = account;
 9     }
10     public void setpictrue(ImageIcon picture) {
11         Picture = picture;
12     }
13     public void setaccount(String account) {
14         Account = account;
15     }
16     public void setnickname(String nickname) {
17         Nickname = nickname;
18     }
19     public void setgender(int gender) {
20         Gender = gender;
21     }
22     public void setlocation(String location) {
23         Location = location;
24     }
25     public void setbirthday(String[] birthday) {
26         Birthday = birthday;
27     }
28     public void setage(int age) {
29         Age = age;
30     }
31     public void setschool(String school) {
32         School = school;
33     }
34     public void setcompany(String company) {
35         Company = company;
36     }
37     public void setjob(String job) {
38         Job = job;
39     }
40     public void setStatu(int statu) {
41         Statu = statu;
42     }
43     public void setautograph(String autograph) {
44         Autograph = autograph;
45     }
46     public ImageIcon getpictrue() {
47         return Picture;
48     }
49     public String getaccount() {
50         return Account;
51     }
52     public String getnickname() {
53         return Nickname;
54     }
55     public int getgender() {
56         return Gender;
57     }
58     public String getlocation() {
59         return Location;
60     }
61     public String[] getbirthday() {
62         return Birthday;
63     }
64 public int getage() {
65         return Age;
66     }
67     public String getschool() {
68         return School;
69     }
70     public String getcompany() {
71         return Company;
72     }
73     public String getjob() {
74         return Job;
75     }
76     public int getStatu() {
77         return Statu;
78     }
79     public String getautograph() {
80         return Autograph;
81     }
82     private ImageIcon Picture;
83     private String Nickname;
84     private String Autograph;
85     private String Account;
86     private int Gender;
87     private String Location;
88     private String[] Birthday = new String[3];
89     private int Age;
90     private String School;
91     private String Company;
92     private String Job;
93     private int Statu;
94 }

5.6.2數據包

定義一個Sender數據包,用來擔任所有的可能數據的包裹,其中維護一個信號,用以告訴服務器需要怎么處理給他的數據包。

下面列出所有可能的信號:

 

SIGINAL

NAME

DETAIL

1

LOGIN

登錄信號,發送一個只有賬號的user對象和一個string類型的password。 檢測完成后,服務器返回,如果不存在該用戶或者密碼錯誤,success=0,否則傳送一個完整user類,success=1。

2

CHECK_EXIST

檢測賬號是否存在信號,發送一個合法郵箱。 檢測完成后,如果不存在該用戶,返回success=0,如果存在返回success=1返回。

3

GET_VERIFICATIONCODE

獲取驗證碼信號,發送一個合法郵箱。

服務器讓郵件服務器發送郵件,並返回一個驗證碼。

4

REGISTER

注冊信號,發送一個具有多個信息的user對象。服務器返回一個success表示是否注冊成功。

5

CHANGEPASSWORD

修改密碼信號,發送一個擁有賬號和新密碼的user對象。服務器返回一個success表示是否修改成功。

6

SEARCH

查找用戶信號,發送一個擁有賬號的user對象。服務器返回一個success表示是否找到該用戶。

7

ADDFRIEND

添加好友信號,發送一個擁有賬號的user對象。服務器返回一個success表示是否添加好友成功。

8

FRUSHLIST

刷新好友列表信號,發送一個擁有賬號的user對象。刷新成功,服務器返回一個vector, success=1,刷新失敗,服務器返回success=0。

9

SAVEPROFILE

保存修改信息信號,發送一個完整的user對象。服務器返回一個success表示是否修改成功。

10

SENDINFO

發送消息信號,傳送兩個擁有賬號的user對象。服務器返回一個success表示是否發送成功。

11

CHANGE_STATU

修改狀態信號,發送一個擁有新的狀態和郵箱賬號的user對象。服務器返回一個success表示是否修改成功。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

在數據包內包含了獲取和設置各種屬性的方法以及多個屬性:數據來源用戶,數據目標用戶,信號,密碼,成功標記,好友列表,驗證碼,聊天消息,數據來源端口號。

 1 public class Sender implements Serializable {
 2     private static final long serialVersionUID = 1L;
 3     static public int LOGIN = 1;
 4     static public int CHECK_EXIST = 2;
 5     static public int GET_VERIFICATIONCODE = 3;
 6     static public int REGISTER = 4;
 7     static public int CHANGEPASSWORD = 5;
 8 static public int SEARCH = 6;
 9     static public int ADDFRIEND = 7;
10     static public int FRUSHLIST = 8;
11     static public int SAVEPROFILE = 9;
12     static public int SENDINFO = 10;
13     static public int CHANGE_STATU = 11;
14 private User from, to;
15     private int siginal;
16     private String password;
17     private int success;
18     private Vector<User> list;
19     private String code;
20     private String chat;
21 private int port;
22     public String getcode() {
23         return code;
24     }
25     public void setport(int port) {
26         this.port = port;
27     }
28     public int getport() {
29         return port;
30     }
31     public User getuserfrom() {
32         return from;
33     }
34     public User getuserto() {
35         return to;
36     }
37     public int getsiginal() {
38         return siginal;
39     }
40     public String getpassowrd() {
41         return password;
42     }
43     public int getsuccess() {
44         return success;
45     }
46     public Vector<User> getlist() {
47         return list;
48     }
49     public void setuserfrom(User from) {
50         this.from = from;
51     }
52     public void setuserto(User to) {
53         this.to = to;
54     }
55     public void setpassword(String password) {
56         this.password = password;
57     }
58     public void setsuccess(int success) {
59         this.success = success;
60     }
61     public void setlist(Vector<User> list) {
62         this.list = list;
63     }
64     public void setcode(String code) {
65         this.code = code;
66     }
67     public Sender(int SIGINAL) {
68         siginal = SIGINAL;
69     }
70     public void setchat(String chat) {
71         this.chat = chat;
72     }
73     public String getchat() {
74         return chat;
75     }
76 }

5.6.3靜態數據

將需要頻繁用的數據作為一個類的靜態數據在程序初始化的時候就全都存進內存,這樣,有兩個好處:1、后期好維護,改一個變量就可以改掉所有的,2、不會重復new相同的數據,導致內存中存在多份相同的數據副本。

這里我們在Common包里定義了4個類用於這樣的存儲。

①       顏色在前端設計中是必不可少的屬性:

 1 public class UColor {
 2     // 登陸界面操作界面顏色
 3     public final static Color OperatorBackgroundColor = new Color(235, 242, 249);
 4     public final static Color RegisterFindWordColor = new Color(38, 133, 227);
 5     public final static Color ButtonDefaultColor = new Color(9, 163, 220);
 6     public final static Color ButtonCoverColor = new Color(60, 195, 245);
 7     public final static Color InputDefaultBorderColor = Color.LIGHT_GRAY;
 8     public final static Color InputCoverBorderColor = new Color(21, 131, 221);
 9     public final static Color InputDefaultWordColor = Color.BLACK;
10     public final static Color InputNoteWordColor = Color.LIGHT_GRAY;
11     public final static Color FindAutoWordColor = new Color(102, 102, 102);
12     public final static Color LoginWordColor = Color.white;
13     public final static Color LoginExitMinButton = new Color(10, 200, 232);
14     // 注冊界面顏色
15     public final static Color RegisterInformationBackgroundColor = new Color(200, 231, 249);
16     public final static Color RegisterHeadBackgroundColor = new Color(18, 183, 245);
17     public final static Color RegisterSubmitBackgroundColor = new Color(142, 203, 241);
18     public final static Color RegisterWarningWordColor = new Color(255, 102, 102);
19     public final static Color RegisterNoteWordColor = new Color(128, 128, 128);
20     public final static Color RegisterAtOnceButtonColor = new Color(138, 201, 112);
21     public final static Color RegisterDefaultBoxBorderColor = Color.BLACK;
22     public final static Color RegisterFocusBoxBorderColor = new Color(93, 158, 243);
23     public final static Color RegisterWarningBoxBorderColor = new Color(222, 0, 29);
24     public final static Color RegisterInformationBoxDefaultBackgroudColor = new Color(200, 231, 249);
25     public final static Color RegisterInformationBoxSuccessBackgroudColor = new Color(194, 241, 64);
26     public final static Color MainHeadPanelBackgroundColor = new Color(40, 138, 221);
27     public final static Color MainUtilPanelBackgroundColor = new Color(207, 229, 248);
28     public final static Color MainMidPanelTabgroundColor = new Color(234, 244, 252);
29     public final static Color MainMidPanelUserLabelCoverBackgroundColor = new Color(252, 240, 193);
30     public final static Color MainMidPanelUserLabelSelectedBackgroundColor = new Color(253, 236, 169);
31     public final static Color MainMidPanelUserLabelAutographColor = new Color(127, 127, 127);
32     public final static Color SearchHasAddedNoteColor = new Color(255, 191, 38);
33     public final static Color ChatMidBackgroundColor = new Color(225, 234, 247);
34     public final static Color CharBubbleColor_blue = new Color(120, 205, 248);
35     public final static Color CharBubbleColor_orange = new Color(253, 196, 0);
36 }

②       這里字體全局通用微軟雅黑,在初始化時先一次性用數組存下從1-25大小的微軟雅黑字體,用於后期很方便的設置字體。

 1 public class UFont {
 2     public static Font[] font = new Font[26];
 3     public static void FontInit() {
 4         Properties props = System.getProperties();
 5         String[] info = props.getProperty("os.name").split(" ");
 6         if (info[0].equals("Windows")) {
 7             UImport.DefaultFont = "font/微軟雅黑.ttf";
 8         }
 9         for (int i = 1; i <= 25; i++) {
10             font[i] = LoadFont.loadFont(UImport.DefaultFont, i);
11         }
12     }
13 }

③       外部文件的路徑同樣需要保存,因為我們很難記住,存下也不用每次去查看。

 1 public class UImport {
 2     public final static String LogoImage = "image/bg.jpg";
 3     public final static String StateBoxIcon = "image/5.jpg";
 4     public final static String UserHeadPicture = "image/4.jpg";
 5     public final static String UserStatuOnline = "image/status/inonline.png";
 6     public final static String UserStatuBusy = "image/status/busy.png";
 7     public final static String UserStatuAway = "image/status/away.png";
 8     public final static String WindowExit1 = "image/nag/exit1.png";
 9     public final static String WindowExit2 = "image/nag/exit2.png";
10     public final static String WindowMin1 = "image/nag/min1.png";
11     public final static String WindowMin2 = "image/nag/min2.png";
12     public final static String MainLogo = "image/logo/mainlogo.png";
13     public final static String MainSearch = "image/search_iconl.png";
14     public final static String MainPeopleDefault = "image/main/people_default.png";
15     public final static String MainPeopleCover = "image/main/people_cover.png";
16     public final static String MainPeopleActive = "image/main/people_active.png";
17     public final static String MainRecentDefault = "image/main/recent_default.png";
18     public final static String MainRecentCover = "image/main/recent_cover.png";
19     public final static String MainRecentActive = "image/main/recent_active.png";
20     public final static String MainUserPicture = "image/5.png";
21     public static String DefaultFont = "font/微軟雅黑.ttf";
22 }

④       在程序中的各種字符串同樣需要存下,主要是能使后期易於維護,而且,萬一是一串很長的字符串不用每次尋找之后再復制。

 1 public class UMap {
 2     public final static String server_ip="127.0.0.1";
 3     public final static String Account = "郵箱";
 4     public final static String Passwd = "密碼";
 5     public final static String Signin = "注冊賬號";
 6     public final static String Find = "找回賬號";
 7     public final static String Remember = "記住密碼";
 8     public final static String Auto = "自動登錄";
 9     public final static String Login = "登  錄";
10     public final static String Cancel = "取  消";
11     public final static String RegisterAtOnce = "立即注冊";
12     public final static String RegisterAccount = "賬號";
13     public final static String RegisterNickName = "昵稱";
14     public final static String RegisterPassword = "密碼";
15     public final static String RegisterConfirmPassword = "確認密碼";
16     public final static String RegisterGender = "性別";
17     public final static String RegisterMale = "男";
18     public final static String RegisterFemale = "女";
19     public final static String RegisterBirthday = "生日";
20     public final static String RegisterLocation = "所在地";
21     public final static String RegisterVerificationCode = "驗證碼";
22     public final static String RegisterAccountWarning = "賬號不可以為空";
23     public final static String RegisterCheckRegistered = "檢查是否已被注冊";
24     public final static String RegisterCheckRegisteredSuccess = "該賬號未被注冊";
25     public final static String RegisterCheckRegisteredFailed = "該賬號已被注冊";
26     public final static String RegisterAccountNote = "輸入一個格式正確的可用郵箱";
27     public final static String RegisterNickNameWarning = "昵稱不可以為空";
28     public final static String RegisterNickNameNote = "請輸入昵稱";
29     public final static String RegisterPasswordWarning = "密碼不可以為空";
30     public final static String RegisterConfirmPasswordNote = "請再次輸入密碼";
31     public final static String RegisterConfirmPasswordWarning = "與輸入密碼不一致";
32     public final static String RegisterVerificationCodeWarning = "驗證碼不能為空";
33     public final static String RegisterVerificationCodeButton = "獲取驗證碼";
34     public final static String RegisterVerificationCodeWrong = "驗證碼錯誤";
35     public final static String RegisterVerificationCodeReset = "秒后可重新獲取";
36     public final static String RegisterPasswordNote = "長度為6-16個字符,不能包含空格,不能是9位以下的純數字";
37     public final static String FindFirst = "第一步:驗證賬號";
38     public final static String FindSecond = "第二步:修改密碼";
39     public final static String FindToSecond = "下一步";
40     public final static String FindComplete = "完成修改";
41     public final static String MainPeople = "聯系人";
42     public final static String MainRecent = "最近聯系";
43     public final static String MainSearch = "查找";
44     public final static String MainFrush = "刷新好友列表";
45     public final static String SearchSearch = "查找好友";
46     public final static String SearchAdd = "添加好友";
47     public final static String SearchHasAdded = "所查找的用戶已經是你的好友";
48     public final static String SearchNone = "沒有查到這個用戶";
49     public final static String ChatExitButton  = "關閉";
50     public final static String ChatSendButton  = "發送";
51 }

在服務器其中,我們同樣有SQLconfig類作為數據庫連接的靜態數據存儲。

 1 public class SQLconfig {
 2     public static final String url = "jdbc:mysql://127.0.0.1/qq?useUnicode=true&characterEncoding=utf8";
 3     public static final String name = "com.mysql.jdbc.Driver";
 4     public static final String user = {username};
 5     public static final String password = {password};
 6     public static final String login = "update user set password=? where account=?";
 7     public static final String register = "insert into user(id,account,password,nickname,autograph,gender,birthday,age,location,school,company,job,statu,create_time) values(null,?,?,?,?,?,?,?,?,?,?,?,?,?)";
 8     public static final String getfriend = "select * from user where id=?";
 9     public static final String check_exist = "select * from user where account=?";
10     public static final String change_statu = "update user set statu=? where account=?";
11     public static final String change_profile = "update user set nickname=? ,autograph=?,gender=?,birthday=?,location=?,school=?,company=?,job=? where account=?";
12     public static final String change_password = "update user set password=? where account=?";
13     public static String sql_get_friend(String account) throws NoSuchAlgorithmException {
14         return "select * from " + getUserList(account);
15     }
16     public static String addFriend(String account) throws NoSuchAlgorithmException {
17         return "insert into " + getUserList(account) + "(id,friend_id) values(null,?)";
18     }
19     public static String sql_create_list(String account) throws NoSuchAlgorithmException {
20         return "create table " + getUserList(account)
21                 + "(id int(32) not null primary key auto_increment,friend_id int(32) not null)";
22     }
23     public static String getUserList(String account) throws NoSuchAlgorithmException {
24         return "List_" + Encryption.getMD5(account);
25     }
26     public static String getChatList(String[] account) throws NoSuchAlgorithmException {
27         Arrays.sort(account);
28         return "Chat_" + Encryption.getMD5(account[0] + account[1]);
29     }
30     public static String sql_create_chat(String[] account) throws NoSuchAlgorithmException {
31         return "create table " + "if not exists " + getChatList(account)
32                 + "(sender_id int(32) not null,content varchar(256) not null ,send_time datetime not null)";
33     }
34     public static String saveChat(String[] account) throws NoSuchAlgorithmException {
35         return "insert into " + getChatList(account) + "(sender_id,content,send_time) values(?,?,?)";
36     }
37 }

5.6.4動態數據

像好友列表這類動態數據,在這里使用了vector可變長的數組,不但迭代方便,而且功能齊全。

1 Vector<User> mainlist;

迭代,向Jtabel中添加對象。

 1     public void localfrushlist() {
 2         DefaultTableModel model = new DefaultTableModel(0, 1);
 3         for (int i = 0; i < mainframe.getlist().size(); i++) {
 4             model.addRow(new Object[] { mainframe.getlist().elementAt(i) });
 5         }
 6         table.setModel(model);
 7         table.setGridColor(UColor.MainMidPanelTabgroundColor);
 8         DefaultTableCellRenderer renderer = new DefaultTableCellRenderer();
 9         renderer.setPreferredSize(new Dimension(0, 0));
10         table.getTableHeader().setDefaultRenderer(renderer);
11         table.getColumnModel().getColumn(0).setCellRenderer(new MainUserCellRender());
12     }

此外,由於相同的好友不能開啟多個聊天框,這里我們設置一個HashMap來一一對應,可以避免這個情況。

1 static HashMap<String, ChatMainFrame> chatframe;
1 else if (e.getClickCount() == 2) {
2     User userto = (User) table.getValueAt(r, c);
3     System.out.println(userto.getaccount() + " " + userto.getnickname());
4     if (mainframe.getmap().get(userto.getaccount()) == null) {
5         ChatMainFrame chatmainframe = new ChatMainFrame(mainframe, userto);
6         mainframe.getmap().put(userto.getaccount(), chatmainframe);
7     } else {                    mainframe.getmap().get(userto.getaccount()).setAlwaysOnTop(true);                mainframe.getmap().get(userto.getaccount()).setAlwaysOnTop(false);
8     }
9 }

當關閉窗口的時候再設置回來。

1 exit.addMouseListener(new MouseAdapter() {
2             ……………………………………
3             public void mouseClicked(MouseEvent e) {
4                 mainframe.getmap().remove(chatuser.getaccount());
5                 chatmainframe.dispose();
6             }
7         });

還有服務器上保存的用戶的ip和port,這里將他們存在一個list中,再用hashMap從賬號映射到list。主要利用的特性是,list<Object>可以放任何的對象。

1 public static HashMap<String, List<Object>> 
2 ip_save = new HashMap<String, List<Object>>();

在邏輯設計中我們可以看到這個HashMap的運用。

5.7網絡編程

5.7.1端口號

因為動態端口的范圍從1024到65535,所以理論上是需要設置端口號為這個范圍之間的就行了。這里,服務器使用9090端口。開啟serversocket用於接受來自客戶端的連接。

1 static int port = 9090; 
2 ServerSocket server = null;
3 try {
4     server = new ServerSocket(port);
5 } catch (IOException e) {
6     e.printStackTrace();
7 }

客戶端的端口限定在10086-11000之間。

1 public static int port = 10086;
1 ServerSocket server = null;
2         for (; MainFrame.port <= 11000; MainFrame.port++) {
3             try {
4                 server = new ServerSocket(MainFrame.port);
5                 break;
6             } catch (IOException e) {
7                 e.printStackTrace();
8             }
9         }

所以,理論上一台電腦可以同時運行915個QQ。

5.7.2序列化

之前定義了Sender類作為數據包,在java中可以進行序列化,來讓Sender對象序列化之后傳入Stream中進行數據傳輸,查詢資料后,發現序列化有一個非常簡單的實現,就是將需要序列化的類實現Serializable接口,並且設置好serialVersionUID,一般1L就行。

1 public class Sender implements Serializable {
2     private static final long serialVersionUID = 1L;
3 ……………………………………………………
4 }
1 public class User implements Serializable {
2     private static final long serialVersionUID = 1L;
3 ……………………………………………………
4 }

這樣在將對象直接傳入ObjectOutputStream對象或者從ObjectInputStream中讀取對象就會自動將對象序列化成一串字節描述或者將字節描述反序列化成對象。

5.7.3對象IO流

我們從socket中獲取輸出輸入流,但往往緩沖區不夠大,也就是說我們無法向流中寫入太大的數據,這樣我們需要在外面套一層BufferedStream,人為設定緩沖區的大小。

1 ObjectOutputStream os = new ObjectOutputStream(
2 new BufferedOutputStream(socket.getOutputStream(), 256 * 1024)
3 );
1 ObjectInputStream is = new ObjectInputStream(
2 new BufferedInputStream(socket.getInputStream(), 256 * 1024)
3 );

5.8多線程

5.8.1計時器

在注冊賬號、找回密碼界面中,都存在獲取驗證碼的按鈕操作,同樣在用戶主界面中也存在刷新好友列表的操作。獲取驗證碼操作如果頻繁的操作將會產生很多問題,包括增大服務器負載,降低賬戶安全性。同樣刷新好友列表,如果在短時間內點擊多次,可能導致數據庫的頻繁訪問導致服務器整體性能的下降。在這里我們使用了計時器,在另一個線程中進行計時,通過計時循環禁用啟用獲取驗證碼的按鈕,包括刷新好友列表同樣如此。

這里很簡單的實現了計時器:

 1 verificationcodebutton.setEnabled(false);
 2 Calendar calendar = Calendar.getInstance();
 3 start = 59;
 4 Date time = calendar.getTime();
 5 Timer timer = new Timer();
 6 timer.scheduleAtFixedRate(new TimerTask() {
 7     public void run() {
 8         verificationcodebutton.setText(start + UMap.RegisterVerificationCodeReset);
 9         start--;
10         if (start == 0) {                                    verificationcodebutton.setText(UMap.RegisterVerificationCodeButton);
11             verificationcodebutton.setEnabled(true);
12             getcodehasclicked = false;
13             timer.cancel();
14         }
15     }
16 }, time, 1000);

5.8.2解除登錄堵塞

我們試想一個這樣的問題,因為網絡較差,我們登錄的比較慢,但我們知道當登錄時,讀入會一直堵塞直到讀到數據或者跳出為止。

1 sender = (Sender) is.readObject();

這樣此時如果我們反悔不想登錄了,應該可以點擊取消並且返回,但此時主線程處在堵塞狀態,我們無法進行任何操作,顯然無法做到取消登錄。

在這里我們通過讓登錄的socket連接和數據傳輸由另一個線程負責,而由主線程繼續保持原本狀態解決這個問題,一旦取消登錄,另一個線程標記為interrupted,由於大部分的時間都用在輸出和等待服務器處理上,所以我們讀取服務器返回信息之前進行interrupted檢測,如果interrupted,直接進行收尾工作,否則將繼續讀取服務器返回信息並進行登錄。

 1 thread = new Thread(new Runnable() {
 2     public void run() {
 3         try {
 4             Socket socket = new Socket(UMap.server_ip, 9090);
 5             User user = new User(UserAccount.getText());
 6             user.setStatu(userstatu);
 7             Sender sender = new Sender(Sender.LOGIN);
 8             sender.setpassword(new String(UserPasswd.getPassword()));
 9             sender.setuserfrom(user);
10             sender.setport(MainFrame.port);
11             ObjectOutputStream os = new ObjectOutputStream(
12 new BufferedOutputStream(socket.getOutputStream(), 256 * 1024));
13             os.writeObject(sender);
14             os.flush();
15             ObjectInputStream is = null;
16             if (!thread.isInterrupted()) {
17                 is = new ObjectInputStream(
18                     new BufferedInputStream(socket.getInputStream(), 256 * 1024));
19                 sender = (Sender) is.readObject();
20                 int dialog;
21                 if (sender.getsuccess() == 0) {
22                     dialog = JOptionPane.showConfirmDialog(mainframe, "賬號不存在或密碼錯誤", "登陸失敗",JOptionPane.YES_OPTION);
23                     if (dialog == 0) {
24                         userhead.setBounds(40, 10, 85, 85);
25                         UserAccount.setVisible(true);
26                         UserAccount.setText("");
27                         UserPasswd.setVisible(true);
28                         UserPasswd.setText("");
29                         CheckRememberPasswd.setVisible(true);
30                         CheckAutoLogin.setVisible(true);
31                         FindButton.setVisible(true);
32                         SigninButton.setVisible(true);
33                         LoginButton.setVisible(true);
34                         StatusBar.setVisible(true);
35                         notebox.setVisible(true);
36                         CancelButton.setVisible(false);
37                     }
38                 } else {
39                         mainframe.dispose();
40                         MainFrame mainframe = new MainFrame(sender.getuserfrom(), sender.getlist());
41                 }
42             }
43 os.close();
44             if (is != null) {
45                 is.close();
46             }
47             socket.close();
48 
49         } catch (IOException e) {
50             // TODO Auto-generated catch block
51             e.printStackTrace();
52         } catch (ClassNotFoundException e) {
53             // TODO Auto-generated catch block
54             e.printStackTrace();
55         }
56     }
57 });
58 thread.start();

5.8.3線程池

在服務器中,我們需要接受來自多個客戶端的socket連接,但是當請求很多時,我們不可能只用一個主線程去排隊一個個運行客戶端的請求,這時我們便需要用到多線程來讓服務器具有同時處理多個任務的能力。但是當我們同時運行的線程過多,會造成內存的大量占用,而線程不斷die又不斷new出來的循環更新迭代也會影響任務的執行效率。現在這里我們需要一個對線程統一管理的東西,它就是線程池,java中自帶了線程池,通過簡單的運用我們可以快速搭建起一個線程池,提高線程的運用率,進而提高程序的運行速度,同時也節省了內存。

1 ThreadPoolExecutor executor = 
2 new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,
3 new ArrayBlockingQueue<Runnable>(5));

這里第一個參數是線程池維護最小線程數,第二個參數是線程池維護最大線程數,第三個是線程池維護線程所允許的空閑時間,第四個是線程池維護線程所允許的空閑時間的單位,這里用的是毫秒,第五個是線程池所使用的緩沖隊列。

這樣我們的ServerSocket一旦Accepted到一個客戶端socket,就只要把socket傳給ServerTask,並讓ServerTask進入多線程中進行執行。

1 while (running) {
2             try {
3                 Socket socket = server.accept();
4                 System.out.println("accepted");
5                 executor.execute(new ServerTask(socket));
6             } catch (IOException e) {
7                 e.printStackTrace();
8             }
9         }
1 public class ServerTask implements Runnable {
2     private Socket socket;
3     private Sender sender;
4     ObjectInputStream is;
5     ObjectOutputStream os;
6     public ServerTask(Socket SOCKET) {
7         socket = SOCKET;
8     }
9     public void run() {

5.9安全性

5.9.1MD5加密

現代數據庫中存儲密碼幾乎都不再使用明文存儲,而是在數據庫中保存好加密后的密碼,這里我們使用了MD5加密算法,這種算法很難進行反向破解,安全性尚可。

 1 public class Encryption {
 2     public static String getMD5(String str) throws NoSuchAlgorithmException {
 3         // 生成一個MD5加密計算摘要
 4         MessageDigest md = MessageDigest.getInstance("MD5");
 5         // 計算md5函數
 6         md.update(str.getBytes());
 7         // digest()最后確定返回md5 hash值,返回值為8為字符串。因為md5 hash值是16位的hex值,實際上就是8位的字符
 8         // BigInteger函數則將8位的字符串轉換成16位hex值,用字符串來表示;得到字符串形式的hash值
 9         return new BigInteger(1, md.digest()).toString(16);
10     }
11 }

在注冊和修改密碼時我們向數據庫中保存密碼的MD5值。

1 sqls4.pst.setString(2, Encryption.getMD5(sender.getpassowrd()));

在登錄時我們將輸入密碼的MD5值與數據庫中的MD5值進行比較。

1 if (Encryption.getMD5(sender.getpassowrd()).equals(exist.getString("password"))) {
2 ……………………………………………………………………
3 }

對於數據庫中表的命名,由於QQ這樣的軟件,數據庫必然是動態的,比如,當一個新的用戶注冊時,必定需要生成一張表來存儲該用戶的好友列表,當一個用戶向另一個用戶初次私聊后,必然會生成一張表,用來存聊天記錄。

因此我們需要巧妙的為數據庫生成不會重復的表,這里使用了這樣的方法,將私聊的兩個用戶的郵箱賬號按字典序升序排列再連接起來,並且使用MD5加密,之所以加密是提高數據庫的安全性。比如產生自 nihao@qq.com與hello@outlook.com 之間的私聊,將自動create一個名為”chat_2f670777c3e46b700eb292c94ff765d2”的表。 nihao@qq.com注冊后會生成一個名為”list_abf647000800514e9456bf7ac160a478”的表。

1 public static String getUserList(String account) throws NoSuchAlgorithmException {
2         return "List_" + Encryption.getMD5(account);
3 }
1 public static String getChatList(String[] account) throws NoSuchAlgorithmException {
2         Arrays.sort(account);
3         return "Chat_" + Encryption.getMD5(account[0] + account[1]);
4     }

5.9.2防注入

現在SQL注入攻擊在網絡上甚囂塵上,怎么樣防止這樣類似的攻擊,查詢JDBC的API之后,發現存在這樣一種方法可以大幅度降低SQL注入攻擊的風險。

 1 public class SQLconnect {
 2     public Connection conn = null;
 3     public PreparedStatement pst = null;
 4     public SQLconnect(String sql) {
 5         try {
 6             Class.forName(SQLconfig.name);// 指定連接類型
 7             conn = DriverManager.getConnection(SQLconfig.url, SQLconfig.user, SQLconfig.password);// 獲取連接
 8             pst = conn.prepareStatement(sql);// 准備執行語句
 9         } catch (Exception e) {
10             e.printStackTrace();
11         }
12     }
13     public void close() {
14         try {
15             this.conn.close();
16             this.pst.close();
17         } catch (SQLException e) {
18             e.printStackTrace();
19         }
20     }
21 }

就是prepareStatement(),這個方法能夠將SQL語句的參數與SQL語法分開,避免了字符串連接,從而加強了安全性。

1 "insert into user(id,account,password,nickname,autograph,gender,birthday,age,location,school
2 ,company,job,statu,create_time) values(null,?,?,?,?,?,?,?,?,?,?,?,?,?)";

5.10邏輯設計

5.10.1服務器通用任務

在ServerTask開始執行時,會首先從socket中獲取ObjectInputStream從而從中讀出sender。

 1 public class ServerTask implements Runnable {
 2     private Socket socket;
 3     private Sender sender;
 4     ObjectInputStream is;
 5     ObjectOutputStream os;
 6     public ServerTask(Socket SOCKET) {
 7         socket = SOCKET;    }
 8     public void run() {
 9         ObjectInputStream is = null;
10         ObjectOutputStream os = null;
11         try {is = new ObjectInputStream(new BufferedInputStream(socket.getInputStream(), 256 * 1024));
12             sender = (Sender) is.readObject();
13             System.out.println(sender);
14         } catch (IOException e1) {
15             e1.printStackTrace();
16         } catch (ClassNotFoundException e) {
17             e.printStackTrace();
18         }
19 …………………………………………

接着就是對sender.SIGINAL的解析分類執行。

 1 switch (sender.getsiginal()) {
 2     case(1):
 3 case(2):
 4 case(3):
 5 case(4): 
 6 case(5):
 7 case(6):
 8 case(7):
 9 case(8):
10 case(9):
11 case(10):
12 case(11):
13 }

5.10.2服務器登錄流程

 1 case (1):
 2             User user1 = sender.getuserfrom();
 3             try {
 4                 SQLconnect sqls1 = new SQLconnect(SQLconfig.check_exist);//連接數據庫,准備好檢查的SQL語句
 5                 sqls1.pst.setString(1, user1.getaccount());//設置空位
 6                 ResultSet exist = sqls1.pst.executeQuery();//執行SQL語句,獲得ResultSet
 7                 // 賬號存在
 8                 if (exist.next()) {
 9                     // 密碼正確
10                     if (Encryption.getMD5(sender.getpassowrd()).equals(exist.getString("password"))) {
11                         user1.setnickname(exist.getString("nickname"));
12                         user1.setautograph(exist.getString("autograph"));//設置用戶信息
13                         user1.setgender(exist.getInt("gender"));
14                         Date birth = exist.getDate("birthday");
15                         Calendar calendar = GregorianCalendar.getInstance();
16                         calendar.setTime(birth);
17                         user1.setbirthday(new String[] { "" + calendar.get(Calendar.YEAR),
18                                 "" + calendar.get(Calendar.MONTH), "" + calendar.get(Calendar.DATE) });
19                         user1.setlocation(exist.getString("location"));
20                         user1.setschool(exist.getString("school"));
21                         user1.setcompany(exist.getString("company"));
22                         user1.setjob(exist.getString("job"));
23                         PreparedStatement upt = sqls1.conn
24                                 .prepareStatement(SQLconfig.sql_get_friend(user1.getaccount()));//准備另一條SQL語句用來獲取好友列表
25                         ResultSet result = upt.executeQuery();
26                         Vector<User> friends = new Vector<User>();
27                         upt = sqls1.conn.prepareStatement(SQLconfig.getfriend);
28                         while (result.next()) {//循環取出好友放進vector中
29                             int id = result.getInt("friend_id");
30                             upt.setInt(1, id);
31                             ResultSet userinfo = upt.executeQuery();
32                             if (userinfo.next()) {
33                                 User user = new User(userinfo.getString("account"));
34                                 user.setnickname(userinfo.getString("nickname"));
35                                 user.setautograph(userinfo.getString("autograph"));
36                                 friends.addElement(user);
37                             }
38                         }
39                         List<Object> li = new LinkedList<>();//登錄后保存該用戶的ip和port,用於其他用戶與他私聊時獲取
40                         li.add(socket.getInetAddress());
41                         li.add(sender.getport());
42                         MainServer.ip_save.put(user1.getaccount(), li);
43                         upt = sqls1.conn.prepareStatement(SQLconfig.change_statu);
44                         upt.setInt(1, user1.getStatu());
45                         upt.setString(2, user1.getaccount());
46                         int isok = upt.executeUpdate();
47                         if (isok > 0) {
48                             sender.setlist(friends);
49                             sender.setsuccess(1);
50                         } else {
51                             sender.setsuccess(0);
52                         }
53                     } else {
54                         sender.setsuccess(0);
55                     }
56                 } else {
57                     sender.setsuccess(0);
58                 }
59                 os = new ObjectOutputStream(new BufferedOutputStream(socket.getOutputStream(), 256 * 1024));
60                 os.writeObject(sender);
61                 os.flush();
62                 os.close();
63                 is.close();
64                 socket.close();
65             } catch (SQLException e) {
66                 // TODO Auto-generated catch block
67                 e.printStackTrace();
68             } catch (NoSuchAlgorithmException e) {
69                 // TODO Auto-generated catch block
70                 e.printStackTrace();
71             } catch (IOException e) {
72                 // TODO Auto-generated catch block
73                 e.printStackTrace();
74             }
75             break;

先檢查賬號存在,再檢查密碼正確,再拉取好友列表,在服務器上記錄ip和端口,最終返回。

5.10.3服務器檢查賬號存在流程

JDBC中,SQL語句查詢執行之后會返回一個ResultSet,只需要判斷ResultSet有沒有next就行,true表示有,false表示沒有。

 1 case (2):
 2             User user2 = sender.getuserfrom();
 3             SQLconnect sqls2 = new SQLconnect(SQLconfig.check_exist); //連接數據庫,准備好檢查的SQL語句
 4             try {
 5                 sqls2.pst.setString(1, user2.getaccount());
 6                 ResultSet result = sqls2.pst.executeQuery();
 7 //檢查返回結果集中有沒有第一個值
 8                 if (result.next()) {
 9                     sender.setsuccess(1);
10                     user2.setnickname(result.getString("nickname"));
11                 } else {
12                     sender.setsuccess(0);
13                 }
14                 sender.setuserfrom(user2);
15                 os = new ObjectOutputStream(new BufferedOutputStream(socket.getOutputStream(), 256 * 1024));
16                 os.writeObject(sender);
17                 os.flush();
18                 os.close();
19                 is.close();
20                 socket.close();
21             } catch (SQLException e) {
22                 e.printStackTrace();
23             } catch (IOException e) {
24                 // TODO Auto-generated catch block
25                 e.printStackTrace();
26             } finally {
27                 sqls2.close();
28             }
29             break;

5.10.4服務器注冊流程

先添加用戶,再創建好友列表。

 1         case (4):
 2             User user4 = sender.getuserfrom();
 3             SQLconnect sqls4 = new SQLconnect(SQLconfig.register);
 4             try {
 5                 sqls4.pst.setBoolean(5, user4.getgender() == 0 ? false : true);
 6                 sqls4.pst.setInt(12, 0);
 7                 sqls4.pst.setString(1, user4.getaccount());
 8                 sqls4.pst.setString(2, Encryption.getMD5(sender.getpassowrd()));
 9                 sqls4.pst.setString(3, user4.getnickname());
10                 sqls4.pst.setString(4, " ");
11                 sqls4.pst.setString(6,
12                         user4.getbirthday()[0] + "-" + user4.getbirthday()[1] + "-" + user4.getbirthday()[2]);
13                 sqls4.pst.setInt(7, 18);
14                 sqls4.pst.setString(8, user4.getlocation());
15                 sqls4.pst.setString(9, " ");
16                 sqls4.pst.setString(10, " ");
17                 sqls4.pst.setString(11, " ");
18                 sqls4.pst.setString(13, new String(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())));
19                 int isok = sqls4.pst.executeUpdate();//更新數據庫獲取更新成功個數
 1 sqls4.conn.prepareStatement(SQLconfig.sql_create_list(user4.getaccount())).executeUpdate();
 2                 sender.setuserfrom(null);
 3                 sender.setpassword(null);
 4                 if (isok > 0) {//如果大於0表示成功
 5                     sender.setsuccess(1);
 6                 } else {
 7                     sender.setsuccess(0);
 8                 }
 9                 os = new ObjectOutputStream(new BufferedOutputStream(socket.getOutputStream(), 256 * 1024));
10                 os.writeObject(sender);
11                 os.flush();
12                 os.close();
13                 is.close();
14                 socket.close();
15             } catch (SQLException e) {
16                 e.printStackTrace();
17             } catch (IOException e) {
18                 // TODO Auto-generated catch block
19                 e.printStackTrace();
20             } catch (NoSuchAlgorithmException e) {
21                 // TODO Auto-generated catch block
22                 e.printStackTrace();
23             }
24             break;

5.10.5服務器修改密碼流程

 1 case (5):
 2             User user5 = sender.getuserfrom();
 3             SQLconnect sqls5 = new SQLconnect(SQLconfig.change_password);//連接數據庫,准備SQL語句
 4             try {
 5                 sqls5.pst.setString(1, Encryption.getMD5(sender.getpassowrd()));//用加密后的MD5比較
 6                 sqls5.pst.setString(2, sender.getuserfrom().getaccount());
 7                 int isok = sqls5.pst.executeUpdate();
 8                 sender.setuserfrom(null);
 9                 sender.setpassword(null);
10                 if (isok > 0) {
11                     sender.setsuccess(1);
12                 } else {
13                     sender.setsuccess(0);
14                 }
15                 os = new ObjectOutputStream(new BufferedOutputStream(socket.getOutputStream(), 256 * 1024));
16                 os.writeObject(sender);
17                 os.flush();
18                 os.close();
19                 is.close();
20                 socket.close();
21             } catch (SQLException e) {
22                 e.printStackTrace();
23             } catch (IOException e) {
24                 e.printStackTrace();
25             } catch (NoSuchAlgorithmException e) {
26                 e.printStackTrace();
27             }
28             break;

5.10.6服務器修改信息流程

這個與修改密碼差不了多少,部分重復代碼省略。

 1 User user9 = sender.getuserfrom();
 2 SQLconnect sqls9 = new SQLconnect(SQLconfig.change_profile);
 3 sqls9.pst.setString(1, user9.getnickname());
 4 sqls9.pst.setString(2, user9.getautograph());
 5 sqls9.pst.setBoolean(3, user9.getgender() == 0 ? false : true);
 6 sqls9.pst.setString(4,user9.getbirthday()[0] + "-" + user9.getbirthday()[1] + "-" + user9.getbirthday()[2]);
 7 sqls9.pst.setString(5, user9.getlocation());
 8 sqls9.pst.setString(6, user9.getschool());
 9 sqls9.pst.setString(7, user9.getcompany());
10 sqls9.pst.setString(8, user9.getjob());
11 sqls9.pst.setString(9, user9.getaccount());
12 int isok = sqls9.pst.executeUpdate();
13 if (isok > 0) {
14     sender.setsuccess(1);
15 } else {
16 sender.setsuccess(0);
17 }
18 sender.setuserfrom(null);

5.10.7服務器聊天流程

客戶端發出請求,接着服務器把消息轉發給好友,途中正好檢查好友登錄狀態真實性。

首先獲取雙方用戶數據:

1 User userfrom = sender.getuserfrom();
2 User userto = sender.getuserto();

接着,找到雙方用戶在數據庫中的id號。

 1 SQLconnect sqls10 = new SQLconnect(
 2 SQLconfig.sql_create_chat(new String[] { userfrom.getaccount(), userto.getaccount() }));
 3 sqls10.pst.executeUpdate();
 4 sqls10.pst = sqls10.conn.prepareStatement(SQLconfig.check_exist);
 5 sqls10.pst.setString(1, userfrom.getaccount());
 6 ResultSet result = sqls10.pst.executeQuery();
 7 int idfrom = 0;
 8 if (result.next()) {
 9     idfrom = result.getInt("id");
10 }
11 sqls10.pst.setString(1, userto.getaccount());
12 result = sqls10.pst.executeQuery();
13 int idto = 0;
14 if (result.next()) {
15     idto = result.getInt("id");
16 }

然后,在服務器保存聊天記錄,如果失敗,則直接返回給客戶端。

 1 sqls10.pst = sqls10.conn.prepareStatement(SQLconfig.saveChat(new String[] { userfrom.getaccount(), userto.getaccount() }));
 2 sqls10.pst.setInt(1, idfrom);
 3 sqls10.pst.setString(2, sender.getchat());
 4 sqls10.pst.setString(3, new String(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())));
 5 int isok = sqls10.pst.executeUpdate();
 6 if (isok > 0) {
 7     sender.setsuccess(1);
 8 } else {
 9     sender.setsuccess(0);
10 }
11 sender.setuserfrom(null);
12 sender.setuserto(null);
13 try {
14     os = new ObjectOutputStream(new BufferedOutputStream(socket.getOutputStream(), 256 * 1024));
15     os.writeObject(sender);
16     os.flush();
17 } catch (IOException e1) {
18     // TODO Auto-generated catch block
19     e1.printStackTrace();
20 }

接着檢查用戶是否在線,如果在線,則把消息發送給對應的用戶。

 1 if (statuto > 0) {
 2     try {
 3         Socket sendsocket = new Socket((InetAddress) MainServer.ip_save.get(userto.getaccount()).get(0),(int) MainServer.ip_save.get(userto.getaccount()).get(1));
 4         sender.setuserfrom(userfrom);
 5         sender.setuserto(userto);
 6         os = new ObjectOutputStream(new BufferedOutputStream(sendsocket.getOutputStream(), 256 * 1024));
 7         os.writeObject(sender);
 8         os.flush();
 9     } catch (IOException e) {
10         sqls10.pst = sqls10.conn.prepareStatement(SQLconfig.change_statu);
11         sqls10.pst.setInt(1, 0);
12         sqls10.pst.setString(2, userto.getaccount());
13         sqls10.pst.executeUpdate();
14         MainServer.ip_save.remove(userto.getaccount());
15         e.printStackTrace();
16     }
17 }

5.10.8客戶端聊天流程

在客戶端點擊發送后,直接給服務器發送,等待服務器回復成功后,加入聊天記錄,重繪聊天窗體。

在另一端,如果收到服務器給他發的消息,則也會加入聊天記錄,如果存在窗體,就會刷新窗體,如果不存在,就只會有提示音。

 1 try {
 2     Socket socket = server.accept();
 3     ObjectInputStream is = new ObjectInputStream(
 4             new BufferedInputStream(socket.getInputStream(), 256 * 1024));
 5     Sender sender = (Sender) is.readObject();
 6     MainFrame.getmessage(sender);
 7 } catch (IOException e) {
 8 e.printStackTrace();
 9 } catch (ClassNotFoundException e) {
10 // TODO Auto-generated catch block
11 e.printStackTrace();
12 }
1 public static void getmessage(Sender sender) {
2 if (chatframe.get(sender.getuserfrom().getaccount()) == null) {
3 Toolkit.getDefaultToolkit().beep();
4 } else {
5         Toolkit.getDefaultToolkit().beep();
6 chatframe.get(sender.getuserfrom().getaccount()).gethistorypanel().addmessage(false, sender.getchat());
7 }
8 }

 

 

 

cq_server

和cq_client一起使用,使用了網絡編程、線程池等

GitHub

直接下載

cq_client

cq的客戶端,在swing上下了點功夫,算是比較好看了,但是代碼寫的很挫,封裝性也很差,主要是學校要求也不高,就瞎幾把寫,復制粘貼。仔細算算,如果 封裝的足夠好的話,至少能減少1000行代碼。

GitHub

直接下載

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM