大家好,欢迎来到IT知识分享网。
直接贴当时的实验报告吧。
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
直接下载
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/34110.html