修改昵称,个性签名,生日,学校,公司,职业,所在地
修改状态
用户交互
添加好友
私聊
一对一聊天
云端存储
存储聊天记录
我使用的开发环境是Eclipse,jdk8.0,数据库使用Mysql。
序号
字段名
数据类型
长度
主键
允许空
默认值
说明
1
id
int
32
是
否
用户编号
2
accout
char
3
pasword
用户密码
4
nickname
64
用户昵称
5
autograph
128
用户个性签名
6
gender
tinyint
用户性别
7
birthday
date
-
用户生日
8
location
用户所在地
9
school
用户学校
10
company
用户公司
11
job
用户职业
12
statu
用户状态
13
create_time
datetime
好友编号
好友的id号
sender_id
发送者的id
content
varchar
256
消息内容
send_time
由于代码太长,大约有6500行,全部贴出来会占很大一部分空间,所以这里只讲一下很关键也很重要,并且有一点难点的实现。
1publicclassLoginRoundTextBoxextendsJTextField{2Colorbordercolor=UColor.InputDefaultBorderColor;3booleanCover=false;4publicvoidBorderHigh(){5bordercolor=UColor.InputCoverBorderColor;6Cover=true;7this.repaint();8}9publicvoidBorderLow(){10bordercolor=UColor.InputDefaultBorderColor;11Cover=false;12this.repaint();13}14protectedvoidpaintBorder(Graphicsg){15inth=getHeight();//从JComponent类获取高宽16intw=getWidth();17Graphics2Dg2d=(Graphics2D)g.create();18Shapeshape=g2d.getClip();19g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);20g2d.setClip(shape);21g2d.setColor(bordercolor);22if(Cover){23g2d.setStroke(newBasicStroke(1.8f));24}else{25g2d.setStroke(newBasicStroke(1.0f));26}27g2d.drawRoundRect(0,0,w-1,h-1,5,5);28g2d.dispose();29super.paintBorder(g2d);30}31}密码框的实现类似,只是需要继承JPasswordField。
1publicclassLoginHeadPanelextendsJPanel{2Imageimage=null;3publicLoginHeadPanel(){4this.setBounds(40,10,85,85);5image=newImageIcon(UImport.UserHeadPicture).getImage();6}7protectedvoidpaintComponent(Graphicsg){8intx=image.getWidth(this);9inty=image.getHeight(this);10g.drawImage(image,0,0,x,y,this);11}12}这是状态选择按钮的实现:
由于大部分软件的注册一般都是在网页中进行注册,这里再次模仿了网页中的设计,在文本框失焦时,进行合法判定并给出一定的提示或警告。
1midpane=newJTabbedPane();2midpane.setPreferredSize(newDimension(402,660));3midpane.setBorder(null);4midpane.setBackground(UColor.MainMidPanelTabgroundColor);5midpane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);6midpane.addTab(null,peopledefaulticon,mainmidpeoplepanel,UMap.MainPeople);7midpane.addTab(null,recentdefaulticon,mainmidrecentpanel,UMap.MainRecent);8midpane.setEnabledAt(1,false);9midpane.setSelectedIndex(0);10this.add(midpane,BorderLayout.CENTER);5.3.2好友列表我个人觉得这是整个前端中最难的,但是通过查阅资料,依然还是比较好的实现了这个界面,首先用JScrollPane打底,让面板可以自动增加滚动条进行滚动。其次,让JScrollPane用JTabel来显示,这样可以很方便的让每一个好友更像表格一样出现在列表中。
但是有个很大难点,怎么样让JTabel里每一个单元格显示3个组件:头像,昵称,个性签名。查阅了很多资料,并且深入内部感受了JTabel的绘制方式,想了一个很巧妙的方法来实现单元格多组件和鼠标覆盖单元格高亮。首先给JTabel添加一个自己继承出来的单元格渲染器TableCellRenderer,并且重写父类的getTableCellRendererComponent方法,其次给表格添加监听器监听鼠标的移动,一旦更换了单元格的位置,就重绘整个表格。
给表格加上MouseMotionListener监听器:
1table.addMouseMotionListener(newMouseMotionAdapter(){2publicvoidmouseMoved(MouseEvente){3intr=table.rowAtPoint(e.getPoint());4if(r!=MainUserCellRender.cover_r){5MainUserCellRender.cover_r=r;6table.repaint();7}8}9});单元格渲染器的继承:
原本并没有类似方法实现这种创意,网上的资料也是少之又少,不全或者直接没有。但是这里同样达到了较好的实现。
使用JSplitPane分割两个面板,并且可以通过拖动,改变两个面板的大小。使其更加人性化。
1JSplitPanejsplitpane=newJSplitPane(JSplitPane.VERTICAL_SPLIT,true,historytextpane,edittextpane);2jsplitpane.setDividerLocation(550);3this.add(jsplitpane,BorderLayout.CENTER);5.4.2气泡这个实现比较难,我这里的实现是使用JPanel作为JScrollPane的显示。扫描发送者输入的文本来计算所需要气泡的大小。
讲整个大面板横向分成多个JPanel,根据气泡的高度动态调整该Jpanel的高度。再在这个小的JPanel里根据发送者重新定位头像的位置和气泡的位置。设定一个最大宽度,遍历输入的所有信息,计算气泡需要的宽度和高度,将他放进小的JPanel中。
小JPanel的实现:
以注册界面的头部面板为例的实现:
1publicclassMouseDragListenerimplementsMouseInputListener{2Pointorigin;3//鼠标拖拽想要移动的目标组件4JFrameframe;5publicMouseDragListener(JFrameframe){6this.frame=frame;7origin=newPoint();8}9publicvoidmouseClicked(MouseEvente){10}11publicvoidmouseEntered(MouseEvente){12}13publicvoidmouseExited(MouseEvente){14this.frame.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));15}16publicvoidmousePressed(MouseEvente){17origin.x=e.getX();18origin.y=e.getY();19}20publicvoidmouseReleased(MouseEvente){21}22publicvoidmouseDragged(MouseEvente){23Pointp=this.frame.getLocation();24this.frame.setLocation(p.x+(e.getX()-origin.x),p.y+(e.getY()-origin.y));25}26publicvoidmouseMoved(MouseEventarg0){27}28}5.6数据设计5.6.1用户类客户端与服务端通用的用户类,用来传递用户的信息。
下面列出所有可能的信号:
SIGINAL
NAME
DETAIL
LOGIN
CHECK_EXIST
GET_VERIFICATIONCODE
服务器让邮件服务器发送邮件,并返回一个验证码。
REGISTER
注册信号,发送一个具有多个信息的user对象。服务器返回一个success表示是否注册成功。
CHANGEPASSWORD
修改密码信号,发送一个拥有账号和新密码的user对象。服务器返回一个success表示是否修改成功。
SEARCH
查找用戶信号,发送一个拥有账号的user对象。服务器返回一个success表示是否找到该用户。
ADDFRIEND
添加好友信号,发送一个拥有账号的user对象。服务器返回一个success表示是否添加好友成功。
FRUSHLIST
刷新好友列表信号,发送一个拥有账号的user对象。刷新成功,服务器返回一个vector,success=1,刷新失败,服务器返回success=0。
SAVEPROFILE
保存修改信息信号,发送一个完整的user对象。服务器返回一个success表示是否修改成功。
SENDINFO
发送消息信号,传送两个拥有账号的user对象。服务器返回一个success表示是否发送成功。
CHANGE_STATU
这里我们在Common包里定义了4个类用于这样的存储。
①颜色在前端设计中是必不可少的属性:
1publicclassUFont{2publicstaticFont[]font=newFont[26];3publicstaticvoidFontInit(){4Propertiesprops=System.getProperties();5String[]info=props.getProperty("os.name").split("");6if(info[0].equals("Windows")){7UImport.DefaultFont="font/微软雅黑.ttf";8}9for(inti=1;i<=25;i++){10font[i]=LoadFont.loadFont(UImport.DefaultFont,i);11}12}13}③外部文件的路径同样需要保存,因为我们很难记住,存下也不用每次去查看。
1Vector
1publicvoidlocalfrushlist(){2DefaultTableModelmodel=newDefaultTableModel(0,1);3for(inti=0;i 1staticHashMap 1exit.addMouseListener(newMouseAdapter(){2……………………………………3publicvoidmouseClicked(MouseEvente){4mainframe.getmap().remove(chatuser.getaccount());5chatmainframe.dispose();6}7});还有服务器上保存的用户的ip和port,这里将他们存在一个list中,再用hashMap从账号映射到list。主要利用的特性是,list 1publicstaticHashMap 因为动态端口的范围从1024到65535,所以理论上是需要设置端口号为这个范围之间的就行了。这里,服务器使用9090端口。开启serversocket用于接受来自客户端的连接。 1staticintport=9090;2ServerSocketserver=null;3try{4server=newServerSocket(port);5}catch(IOExceptione){6e.printStackTrace();7}客户端的端口限定在10086-11000之间。 之前定义了Sender类作为数据包,在java中可以进行序列化,来让Sender对象序列化之后传入Stream中进行数据传输,查询资料后,发现序列化有一个非常简单的实现,就是将需要序列化的类实现Serializable接口,并且设置好serialVersionUID,一般1L就行。 1publicclassSenderimplementsSerializable{2privatestaticfinallongserialVersionUID=1L;3……………………………………………………4}1publicclassUserimplementsSerializable{2privatestaticfinallongserialVersionUID=1L;3……………………………………………………4}这样在将对象直接传入ObjectOutputStream对象或者从ObjectInputStream中读取对象就会自动将对象序列化成一串字节描述或者将字节描述反序列化成对象。 我们从socket中获取输出输入流,但往往缓冲区不够大,也就是说我们无法向流中写入太大的数据,这样我们需要在外面套一层BufferedStream,人为设定缓冲区的大小。 这里很简单的实现了计时器: 这样我们的ServerSocket一旦Accepted到一个客户端socket,就只要把socket传给ServerTask,并让ServerTask进入多线程中进行执行。 1while(running){2try{3Socketsocket=server.accept();4System.out.println("accepted");5executor.execute(newServerTask(socket));6}catch(IOExceptione){7e.printStackTrace();8}9}1publicclassServerTaskimplementsRunnable{2privateSocketsocket;3privateSendersender;4ObjectInputStreamis;5ObjectOutputStreamos;6publicServerTask(SocketSOCKET){7socket=SOCKET;8}9publicvoidrun(){5.9安全性5.9.1MD5加密现代数据库中存储密码几乎都不再使用明文存储,而是在数据库中保存好加密后的密码,这里我们使用了MD5加密算法,这种算法很难进行反向破解,安全性尚可。 1publicclassEncryption{2publicstaticStringgetMD5(Stringstr)throwsNoSuchAlgorithmException{3//生成一个MD5加密计算摘要4MessageDigestmd=MessageDigest.getInstance("MD5");5//计算md5函数6md.update(str.getBytes());7//digest()最后确定返回md5hash值,返回值为8为字符串。因为md5hash值是16位的hex值,实际上就是8位的字符8//BigInteger函数则将8位的字符串转换成16位hex值,用字符串来表示;得到字符串形式的hash值9returnnewBigInteger(1,md.digest()).toString(16);10}11}在注册和修改密码时我们向数据库中保存密码的MD5值。 1publicstaticStringgetUserList(Stringaccount)throwsNoSuchAlgorithmException{2return"List_"+Encryption.getMD5(account);3}1publicstaticStringgetChatList(String[]account)throwsNoSuchAlgorithmException{2Arrays.sort(account);3return"Chat_"+Encryption.getMD5(account[0]+account[1]);4}5.9.2防注入现在SQL注入攻击在网络上甚嚣尘上,怎么样防止这样类似的攻击,查询JDBC的API之后,发现存在这样一种方法可以大幅度降低SQL注入攻击的风险。 1publicclassSQLconnect{2publicConnectionconn=null;3publicPreparedStatementpst=null;4publicSQLconnect(Stringsql){5try{6Class.forName(SQLconfig.name);//指定连接类型7conn=DriverManager.getConnection(SQLconfig.url,SQLconfig.user,SQLconfig.password);//获取连接8pst=conn.prepareStatement(sql);//准备执行语句9}catch(Exceptione){10e.printStackTrace();11}12}13publicvoidclose(){14try{15this.conn.close();16this.pst.close();17}catch(SQLExceptione){18e.printStackTrace();19}20}21}就是prepareStatement(),这个方法能够将SQL语句的参数与SQL语法分开,避免了字符串连接,从而加强了安全性。 1"insertintouser(id,account,password,nickname,autograph,gender,birthday,age,location,school2,company,job,statu,create_time)values(null,,,,,,,,,,,,,)";5.10逻辑设计5.10.1服务器通用任务在ServerTask开始执行时,会首先从socket中获取ObjectInputStream从而从中读出sender。 1publicclassServerTaskimplementsRunnable{2privateSocketsocket;3privateSendersender;4ObjectInputStreamis;5ObjectOutputStreamos;6publicServerTask(SocketSOCKET){7socket=SOCKET;}8publicvoidrun(){9ObjectInputStreamis=null;10ObjectOutputStreamos=null;11try{is=newObjectInputStream(newBufferedInputStream(socket.getInputStream(),256*1024));12sender=(Sender)is.readObject();13System.out.println(sender);14}catch(IOExceptione1){15e1.printStackTrace();16}catch(ClassNotFoundExceptione){17e.printStackTrace();18}19…………………………………………接着就是对sender.SIGINAL的解析分类执行。 JDBC中,SQL语句查询执行之后会返回一个ResultSet,只需要判断ResultSet有没有next就行,true表示有,false表示没有。 1case(2):2Useruser2=sender.getuserfrom();3SQLconnectsqls2=newSQLconnect(SQLconfig.check_exist);//连接数据库,准备好检查的SQL语句4try{5sqls2.pst.setString(1,user2.getaccount());6ResultSetresult=sqls2.pst.executeQuery();7//检查返回结果集中有没有第一个值8if(result.next()){9sender.setsuccess(1);10user2.setnickname(result.getString("nickname"));11}else{12sender.setsuccess(0);13}14sender.setuserfrom(user2);15os=newObjectOutputStream(newBufferedOutputStream(socket.getOutputStream(),256*1024));16os.writeObject(sender);17os.flush();18os.close();19is.close();20socket.close();21}catch(SQLExceptione){22e.printStackTrace();23}catch(IOExceptione){24//TODOAuto-generatedcatchblock25e.printStackTrace();26}finally{27sqls2.close();28}29break;5.10.4服务器注册流程先添加用户,再创建好友列表。 首先获取双方用户数据: 1Useruserfrom=sender.getuserfrom();2Useruserto=sender.getuserto();接着,找到双方用户在数据库中的id号。 1SQLconnectsqls10=newSQLconnect(2SQLconfig.sql_create_chat(newString[]{userfrom.getaccount(),userto.getaccount()}));3sqls10.pst.executeUpdate();4sqls10.pst=sqls10.conn.prepareStatement(SQLconfig.check_exist);5sqls10.pst.setString(1,userfrom.getaccount());6ResultSetresult=sqls10.pst.executeQuery();7intidfrom=0;8if(result.next()){9idfrom=result.getInt("id");10}11sqls10.pst.setString(1,userto.getaccount());12result=sqls10.pst.executeQuery();13intidto=0;14if(result.next()){15idto=result.getInt("id");16}然后,在服务器保存聊天记录,如果失败,则直接返回给客户端。 1sqls10.pst=sqls10.conn.prepareStatement(SQLconfig.saveChat(newString[]{userfrom.getaccount(),userto.getaccount()}));2sqls10.pst.setInt(1,idfrom);3sqls10.pst.setString(2,sender.getchat());4sqls10.pst.setString(3,newString(newSimpleDateFormat("yyyy-MM-ddHH:mm:ss").format(newDate())));5intisok=sqls10.pst.executeUpdate();6if(isok>0){7sender.setsuccess(1);8}else{9sender.setsuccess(0);10}11sender.setuserfrom(null);12sender.setuserto(null);13try{14os=newObjectOutputStream(newBufferedOutputStream(socket.getOutputStream(),256*1024));15os.writeObject(sender);16os.flush();17}catch(IOExceptione1){18//TODOAuto-generatedcatchblock19e1.printStackTrace();20}接着检查用户是否在线,如果在线,则把消息发送给对应的用户。 1if(statuto>0){2try{3Socketsendsocket=newSocket((InetAddress)MainServer.ip_save.get(userto.getaccount()).get(0),(int)MainServer.ip_save.get(userto.getaccount()).get(1));4sender.setuserfrom(userfrom);5sender.setuserto(userto);6os=newObjectOutputStream(newBufferedOutputStream(sendsocket.getOutputStream(),256*1024));7os.writeObject(sender);8os.flush();9}catch(IOExceptione){10sqls10.pst=sqls10.conn.prepareStatement(SQLconfig.change_statu);11sqls10.pst.setInt(1,0);12sqls10.pst.setString(2,userto.getaccount());13sqls10.pst.executeUpdate();14MainServer.ip_save.remove(userto.getaccount());15e.printStackTrace();16}17}5.10.8客户端聊天流程在客户端点击发送后,直接给服务器发送,等待服务器回复成功后,加入聊天记录,重绘聊天窗体。 在另一端,如果收到服务器给他发的消息,则也会加入聊天记录,如果存在窗体,就会刷新窗体,如果不存在,就只会有提示音。 1try{2Socketsocket=server.accept();3ObjectInputStreamis=newObjectInputStream(4newBufferedInputStream(socket.getInputStream(),256*1024));5Sendersender=(Sender)is.readObject();6MainFrame.getmessage(sender);7}catch(IOExceptione){8e.printStackTrace();9}catch(ClassNotFoundExceptione){10//TODOAuto-generatedcatchblock11e.printStackTrace();12}1publicstaticvoidgetmessage(Sendersender){2if(chatframe.get(sender.getuserfrom().getaccount())==null){3Toolkit.getDefaultToolkit().beep();4}else{5Toolkit.getDefaultToolkit().beep();6chatframe.get(sender.getuserfrom().getaccount()).gethistorypanel().addmessage(false,sender.getchat());7}8}