目录
Helllo!你好哇,我是灰小猿!一个超会写Bug的程序猿!
最近在进行网络通信的学习时,突发奇想就想模仿微信做一个简单的网络聊天室,所以今天在这里记录一下开发过程。
先看一波效果图:
编辑
包括消息输入框和消息接收框两大块,两个用户(客户端和服务器)之间可以实时的进行消息的通信。
总体开发思路
网络聊天室的开发是基于TCP/IP协议而建立,通过指定的IP地址和端口号进行实时的通信,关于使用TCP/IP协议通信的基础学习,可以看我的这篇博客“Java利用TCP协议实现客户端与服务器通信”。这上面对Java语言建立TCP协议及套接字的使用做了较为详细的讲解。
首先记录一下聊天室项目开发的总体思路:
- 设计并完成客户端和服务器的交互界面
- 建立各个按钮的内部监听类或监听函数
- 客户端和服务器基于TCP/IP协议建立通信
- 分别编写客户端和服务器通信线程,对双方消息的发送和接收进行监听
- 编写通信断开函数,实现网络通信的可断开
好了,以上五个步骤是主要的开发过程,其中还有很多需要编写和注意的小细节,接下来分享一下网络聊天室项目的详细开发思路,同时附上对应的源码:
服务器端
服务器界面设计
服务器端的界面设计上,主要包括的元素是:连接、断开、发送按钮、消息输入框、消息接收框、端口号输入框等,根据PC端微信的界面原理,可以根据自己的想法简单设计,我设计的服务器端的界面如下:
编辑
之后根据设计依次要完成的是:
建立TCP服务器端通信
根据TCP协议通信原理,在服务器端需要基于端口号建立通信协议,之后在客户端以相同的方式建立客户端套接字来实现通信连接。
在服务器端,按照实际需要,网络通信应该是在用户输入通信端口号并且点击了连接按钮之后,再进行服务器端通信的,因此该段代码应该写在连接按钮的监听函数中去,在这里是建立了连接按钮内部监听类:
//设置连接按钮内部监听类 class ConnectJBClass implements ActionListener{ @Override public void actionPerformed(ActionEvent e) { //判断端口输入框是否为空 if (portText.getText().equals("")) { JOptionPane.showMessageDialog(null, "请输入连接的端口号!", "提示", JOptionPane.ERROR_MESSAGE); } else { //如果输入的端口不是整型,则异常抛出 try { port = Integer.parseInt(portText.getText()); //获取到用户输入的端口号 isCorrectPort = true; //如果用户输入的端口号正确,就设置为true } catch (Exception e2) { // TODO: handle exception JOptionPane.showMessageDialog(null, "请输入正确的端口号!", "提示", JOptionPane.ERROR_MESSAGE); } //如果输入了正确格式的端口号则继续,否则不执行 if (isCorrectPort) { try { server = new ServerSocket(port); //建立服务器,端口为用户输入port stateJL.setText("正在等待连接..."); System.out.println("端口号是“" + Integer.toString(port) + "”"); client = server.accept(); //调用服务器函数对客户端进行连接 stateJL.setText("IP:" + client.getInetAddress()); isConnect = true; //建立通信 threadConnect.start();//连接成功后启动通信线程 } catch (IOException e1) { JOptionPane.showMessageDialog(null, "客户端已断开!", "提示", JOptionPane.ERROR_MESSAGE); } } } } }
建立服务器消息发送输出流
在通信建立完成之后,就是实现双方的信息交互了,首先我们应该完成客户端到服务器或服务器到客户端的单向通信,之后再进行双向通信,在这里我们先完成服务器端向客户端发送,客户端向服务器通信使用的是同样的方法,该代码同样是在发送按钮的内部监听类中完成:
//设置发送按钮内部监听类 class ShendJBClass implements ActionListener{ @Override public void actionPerformed(ActionEvent e) { try { String putText = sendWindow.getText(); //获取到服务器用户输入的文本 String putTime = getTime(); setInfoWindosFont(putTime, Color.blue, false, 15); setInfoWindosFont(putText, Color.black, false, 20); sendWindow.setText(""); //发送完毕后将发送框清空 OutputStream put = client.getOutputStream(); //定义发送给客户端的输出流 put.write(putText.getBytes()); //将文本转为字节发送 } catch (IOException e1) { // TODO Auto-generated catch block } } }
建立服务器消息接收输入流
这一步要完成的是对客户端发送过来的消息进行接收,在这里根据TCP/IP协议,要建立消息输入流对象,从而实现对消息的接收:客户端接收是以同样的方法,具体代码如下:
try { InputStream iStream = client.getInputStream(); //获取到客户端的输入流 byte [] b = new byte[1024]; int len = iStream.read(b); //以二进制的形式对数据进行读取 String data = new String(b,0,len); //接收到的内容 String infoTime = getTime(); //消息发送的时间 setInfoWindosFont(infoTime, Color.red, false, 15); setInfoWindosFont(data, Color.black, false, 20); /*使滚动条置于文本框最下端*/ infoWindow.setSelectionStart(infoWindow.getText().length()); JScrollBar jSBInfo = jScrollPaneInfo.getVerticalScrollBar(); jSBInfo.setValue(jSBInfo.getMaximum()); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); }
建立服务器实时消息通信线程
以上我们基本就完成了客户端和服务器的双向通信,也就是说我们的客户端可以接收到来自服务器的消息,服务器同时也可以接收到来自客户端的消息,但是需要注意的是,以上我们建立的通信是单次的,也就是说只能实现一次发送和接收,那么很显然这样是不可以的,实际开发的聊天室应该是可以在通信畅通的前提下一直通信的才对,所以我们接下来就是要解决这样的问题。
实现客户端和服务器实时通信的方法其实很简单,我们只需要对客户端或者服务器发送的消息实时的监听,只要一旦监听到有消息的发送,那么我们就将接收到的消息在对应的消息框显示出来,所以这里要使用线程的方法,具体代码如下:
//通信线程 threadConnect = new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub while (true) { //如果当前已经建立连接 if (isConnect) { try { InputStream iStream = client.getInputStream(); //获取到客户端的输入流 byte [] b = new byte[1024]; int len = iStream.read(b); //以二进制的形式对数据进行读取 String data = new String(b,0,len); //接收到的内容 String infoTime = getTime(); //消息发送的时间 setInfoWindosFont(infoTime, Color.red, false, 15); setInfoWindosFont(data, Color.black, false, 20); /*使滚动条置于文本框最下端*/ infoWindow.setSelectionStart(infoWindow.getText().length()); JScrollBar jSBInfo = jScrollPaneInfo.getVerticalScrollBar(); jSBInfo.setValue(jSBInfo.getMaximum()); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } else { stateJL.setText("服务器未连接!"); break; } } } });
设置服务器通信自由断开
在以上完成之后,我们的聊天室就可以实现双向的实时通信了,但是这也仅仅是通信,就像我们在使用微信的时候,还有对方下线的情况出现对吧,所以在这里我们同样是建立了一个客户端和服务器的断开设置。在点击了断开按钮之后,我们的客户端和服务器就无法通信了,其实很简单只需要将客户端和服务器的套接字close掉就可以了,具体代码如下:
//设置断开连接按钮内部监听类 class CloseConnectJBClass implements ActionListener{ @Override public void actionPerformed(ActionEvent e) { // TODO Auto-generated method stub isConnect = false; //服务器断开 try { server.close(); //中止服务器端运行 } catch (IOException e1) { // TODO Auto-generated catch block JOptionPane.showMessageDialog(null, "服务器端已断开!", "提示", JOptionPane.ERROR_MESSAGE); } //JOptionPane.showMessageDialog(null, "服务器端已断开!", "提示", JOptionPane.ERROR_MESSAGE); } }
客户端
在客户端我们同样需要按照和服务器端一样的思路进行编写,
客户端界面设计
首先是界面的设计,效果如下:
编辑
建立TCP客户端通信
与服务器端稍微有所不同的是,客户端的通信是基于IP地址和端口号的,也就是说在建立客户端通信时,我们需要输入通信的IP地址还有和服务器端一样的端口号,这样才能建立双方的通信。建立客户端通信是在客户端的连接按钮中实现的,这里建立客户端连接按钮内部监听类:
//为连接按钮添加内部事件监听类 class ConnectJBClass implements ActionListener{ @Override public void actionPerformed(ActionEvent e) { //判断端口输入框和IP输入框是否为空 if (portText.getText().equals("端口号")||ipTextArea.getText().equals("IP地址")) { JOptionPane.showMessageDialog(null, "请输入完整的IP和端口号!", "提示", JOptionPane.ERROR_MESSAGE); } else { //对用户输入的格式进行判断 try { ipClient = ipTextArea.getText(); //获取到用户输入的IP //如果IP正确 if (isCorrectIp2(ipClient)) { isCorrectIp = true; } else { JOptionPane.showMessageDialog(null, "请输入正确格式的IP!", "提示", JOptionPane.ERROR_MESSAGE); } port = Integer.parseInt( portText.getText()); //获取到用户输入的端口号 isCorrectPort = true; //System.out.println("端口正确!"); } catch (Exception e2) { // TODO: handle exception JOptionPane.showMessageDialog(null, "请输入正确格式的端口号!", "提示", JOptionPane.ERROR_MESSAGE); } //如果用户输入的IP和端口格式都正确 if (isCorrectIp&&isCorrectPort) { JOptionPane.showMessageDialog(null, "输入完成,正在连接......\nIP:" + ipClient, "提示", JOptionPane.ERROR_MESSAGE); /*******判断正确后将判断变量赋初值,以便下次输入判断********/ isCorrectIp = false; isCorrectPort = false; try { client = new Socket(ipClient,port); //建立客户端 stateJL.setText("客户端连接成功!"); isConnect = true; //已经建立连接 threadConnect.start();//启动通信线程 } catch (IOException e1) { // TODO Auto-generated catch block JOptionPane.showMessageDialog(null, "客户端连接失败!", "提示", JOptionPane.ERROR_MESSAGE); } } } } }
建立客户端消息发出输出流
//设置发送按钮内部监听类 class ShendJBClass implements ActionListener{ @Override public void actionPerformed(ActionEvent e) { try { String putText = sendWindow.getText(); //获取到客户端用户输入的文本 String putTime = getTime(); setInfoWindosFont(putTime, Color.blue, false, 15); setInfoWindosFont(putText, Color.black, false, 20); sendWindow.setText(""); //发送完毕后将发送框清空 OutputStream put = client.getOutputStream(); //定义发送给服务器的输出流 put.write(putText.getBytes()); //将文本转为字节发送 } catch (IOException e1) { } } }
建立客户端消息接收输入流
try { InputStream input = client.getInputStream(); byte [] infoByte = new byte[1024]; int len = input.read(infoByte); String infoTime = getTime(); //获取当前时间 String data = new String(infoByte,0,len); //获取接收的消息 String oldText = infoWindow.getText(); //获取到之前文本框的内容 String atText = oldText + "\n" + infoTime + "\n" + data; //将要在文本框显示的内容 System.out.println(atText); setInfoWindosFont(infoTime, Color.RED, false, 15); setInfoWindosFont(data, Color.black, false, 20); //infoWindow.setText(atText); /*使滚动条置于文本框最下端*/ infoWindow.setSelectionStart(infoWindow.getText().length()); JScrollBar jSBInfo = jScrollPaneInfo.getVerticalScrollBar(); jSBInfo.setValue(jSBInfo.getMaximum()); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); }
建立客户端实时通信线程
//通信线程 threadConnect = new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub while (true) { if (isConnect) { stateJL.setText("正在通信!"); try { InputStream input = client.getInputStream(); byte [] infoByte = new byte[1024]; int len = input.read(infoByte); String infoTime = getTime(); //获取当前时间 String data = new String(infoByte,0,len); //获取接收的消息 String oldText = infoWindow.getText(); //获取到之前文本框的内容 String atText = oldText + "\n" + infoTime + "\n" + data; //将要在文本框显示的内容 System.out.println(atText); setInfoWindosFont(infoTime, Color.RED, false, 15); setInfoWindosFont(data, Color.black, false, 20); //infoWindow.setText(atText); /*使滚动条置于文本框最下端*/ infoWindow.setSelectionStart(infoWindow.getText().length()); JScrollBar jSBInfo = jScrollPaneInfo.getVerticalScrollBar(); jSBInfo.setValue(jSBInfo.getMaximum()); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } else { stateJL.setText("客户端已断开......"); break; } } } });
设置客户端通信自由断开
//设置断开连接按钮内部监听类 class CloseConnectJBClass implements ActionListener{ @Override public void actionPerformed(ActionEvent e) { isConnect = false; //服务器断开 //JOptionPane.showMessageDialog(null, "客户器端已断开!", "提示", JOptionPane.ERROR_MESSAGE); try { client.close(); //中止客户端运行 } catch (IOException e1) { JOptionPane.showMessageDialog(null, "客户器端已断开!", "提示", JOptionPane.ERROR_MESSAGE); } } }
在客户端和服务器都完成之后,这样我们的服务器和客户端就实现了可连接可断开的双向实时通信,
但是在进行实际开发时,还有很多需要注意的小细节,大灰狼在这里一一列出。
获取当前时间函数
为了可以实时的观察到我们发送和接收的消息的时间,在这里需要有一个获取当前时间的函数。代码如下:
//定义获取当前时间的方法 public String getTime() { Date date = new Date(); SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy"); SimpleDateFormat monthFormat = new SimpleDateFormat("MM"); SimpleDateFormat dayFormat = new SimpleDateFormat("dd"); SimpleDateFormat hourFormat = new SimpleDateFormat("HH"); SimpleDateFormat minuteFormat = new SimpleDateFormat("mm"); SimpleDateFormat secondFormat = new SimpleDateFormat("ss"); String year = yearFormat.format(date); String month = monthFormat.format(date); String day = dayFormat.format(date); String hour = hourFormat.format(date); String minute = minuteFormat.format(date); String second = secondFormat.format(date); return year + ":" + month + ":" + day + ":" + hour + ":" + minute + ":" + second; }
文本框内容显示不同效果
从上面的效果展示中我们可以看到在文本框中我们显示的时间以及客户端服务器发送的消息显示的字体颜色及属性是不一样的,
编辑
我们知道,在实际应用中文本框是纯文本的形式,是无法实现上述效果的,因此对于消息接收框,我们使用的是JTextPane文本域,并且在其中设置我们想要显示的字体样式,关于如何显示的具体教程可以看我的这篇文章“Java文本框内文字显示不同颜色、字号等属性”,函数代码如下:
//设置接收框文本字体属性 public void setInfoWindosFont(String str, Color col,boolean bold,int fontSize) { SimpleAttributeSet attrSet = new SimpleAttributeSet(); StyleConstants.setForeground(attrSet, col);//设置颜色 if (bold) { StyleConstants.setBold(attrSet, bold);//设置粗体 } StyleConstants.setFontSize(attrSet, fontSize);//设置字号 /*********infoWindow为JTextPane文本域的名称*****************/ Document doc = infoWindow.getDocument(); str = "\n" + str; try { doc.insertString(doc.getLength(), str, attrSet); } catch (BadLocationException e) { // TODO Auto-generated catch block //e.printStackTrace(); JOptionPane.showMessageDialog(null, "字体设置错误!", "提示", JOptionPane.ERROR_MESSAGE); } }
判断一个字符串是否为IP地址
我们在编写客户端时,需要输入网络通信的IP地址,那么我们就需要判断用户输入的IP地址是否正确,以防程序出错,因此在这里需要对输入的字符串(IP地址)进行判断,关于判断一个字符串是否为IP地址的详细讲解可以看这篇博客“算法-判断字符串是否为IP地址”,这里我列出一个在程序中使用到的判断方法,该方法基于正则表达式判断,代码如下:
//利用正则表达式判断字符是否为IP public boolean isCorrectIp2(String ipString) { String ipRegex = "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"; //IP地址的正则表达式 //如果前三项判断都满足,就判断每段数字是否都位于0-255之间 if (ipString.matches(ipRegex)) { String[] ipArray = ipString.split("\\."); for (int i = 0; i < ipArray.length; i++) { int number = Integer.parseInt(ipArray[i]); //4.判断每段数字是否都在0-255之间 if (number <0||number>255) { return false; } } return true; } else { return false; //如果与正则表达式不匹配,则返回false } }
好了,关于网络聊天室的开发就记录到这里,
完整的源码可以在以下链接获取:
点击获取完整源码 提取码:2heo
觉得不错记得点赞关注哟!
大灰狼陪你一起进步!
编辑