用Java Socket制作文字聊天小程序
1.程序实现的功能
两个客户端之间,实现在线文字聊天,和接收离线消息。
2.程序总体结构
程序整体是C/S结构,用java中socket通信建立服务端和客户端之间的UDP连接,消息都通过服务端转发,客户端之间不直接建立连接。
3.服务端介绍
(图3.1 服务端初始界面)
首先,服务端程序运行后,需要点击启动按钮,基于事件监听机制,建立socket连接和等待接收数据报都在按钮actionPerformed函数里进行:
1 BStart.addActionListener(new ActionListener() { //启动按钮 2 3 @Override 4 public void actionPerformed(ActionEvent e) { 5 thread t1 = new thread(); 6 t1.start(); 7 } 8 });
注意到,按钮里的并没有socket操作,只有一个线程的执行,其实,socket操作就放在这个线程里进行,之所以用到线程,原因在后文会给出。
通过继承了Thread类的方式实现多线程:
1 static class thread extends Thread{ 2 public thread(){ 3 super(); 4 } 5 public void run(){ 6 //socket methods... 7 }
第一件事是建立socket,绑定到本机,并显示在左边的系统消息框内:
1 //建立数据报Socket并显示 2 InetAddress addr = InetAddress.getByName("localhost"); 3 DatagramSocket ds =new DatagramSocket(PORT,addr); 4 ta.setText("【" + df.format(new Date()) +"】" + "UDP服务器已启动:" + addr.getHostAddress() + addr.getHostName());
(3.2 服务端启动连接界面)
接下里通过DatagramSocket的receive函数等待数据报的到来:
1 while(true){ 2 ds.receive(inDataPacket);//等待数据报的到来 3 //... 4 }
如果收到的是客户端的连接请求,则更新用户列表,返回确认连接报文,并在系统消息框内显示客户端连接的消息:
1 //数据的处理 2 String str = new String(inDataPacket.getData(), 0, inDataPacket.getLength()); 3 if(str.equals(new String("Request Connect"))){ //连接请求 4 String str2 = inDataPacket.getAddress() + "(" + inDataPacket.getPort() + ")"; 5 //更新用户列表 6 if(ta2.getText().indexOf(str2) == -1){ 7 String history2 = ta2.getText(); 8 String now2 = String.format("%s%n", history2) + str2; 9 ta2.setText(now2); 10 } 11 12 //返回确认连接报文 13 String strRep = "Connection Confirm"; 14 DatagramPacket outDataPacket = new DatagramPacket(strRep.getBytes(),strRep.length(), 15 inDataPacket.getAddress(), inDataPacket.getPort()); 16 ds.send(outDataPacket); 17 18 //更新系统消息列表 19 String history_SM = ta.getText(); 20 String now_SM = String.format("%s%n", history_SM) + "【" + df.format(new Date()) +"】" +str2 + "成功连接到服务器"; 21 ta.setText(now_SM);
(图3.3 客户端连接到服务器)
如果不是请求连接报文,则为消息报文。数据报中消息的头部加上了源端口和目的端口,方便服务端进行数据报的封装,如果源端口为8888,目的端口为8889,则消息的头部为:TO8889FR8888。如果用户在线(在当前用户列表上查找,有则判断在线),则封装数据报并投递。如果不在线,则将消息储存,等待此用户上线后再发送该消息。只要收到消息,系统显示框都会有显示。
1 //消息处理 2 String Port = str.substring(2, 6); //提取收信方端口 3 if(ta2.getText().indexOf(Port) != -1){ //用户在线 4 String strRep = str; 5 DatagramPacket outDataPacket = new DatagramPacket(strRep.getBytes(),strRep.getBytes().length, 6 addr, Integer.parseInt(Port)); 7 ds.send(outDataPacket); 8 9 }else{ //用户不在线 10 if(Port.equals(new String("8888"))){ 11 S1 = str; 12 }else if(Port.equals(new String("8889"))){ 13 S2 = str; 14 }else{ 15 S3 = str; 16 } 17 } 18 String history = ta.getText(); 19 String now = String.format("%s%n", history) + "【" + df.format(new Date()) +"】" + str.subSequence(8, 12) + "给" + Port + "发送了一条消息。"; 20 ta.setText(now);
(图3.4 服务端显示消息发送成功)
4.客户端介绍
客户端和服务端有很多类似,不管是界面还是代码结构。为了演示的需要,共设置三个客户端,由于都在本机运行,地址全为localhost,以端口号区分。
(图4.1 客户端初始界面)
客户端的socket操作放在连接按钮的监听函数里,完成消息的获取:
1 //建立数据报socket 2 addr = InetAddress.getByName("localhost"); 3 datagramSocket = new DatagramSocket(LocalPORT); 4 datagramSocket.setSoTimeout(3000); //设置3秒超时 5 6 //发送登录消息给服务器 7 String str = "Request Connect"; 8 DatagramPacket login = new DatagramPacket(str.getBytes(), str.length(), addr, ServerPORT); 9 datagramSocket.send(login); 10 11 //接受服务器的确认数据报 12 byte[] msg = new byte[100]; 13 DatagramPacket inDataPacket = new DatagramPacket(msg, msg.length); 14 datagramSocket.receive(inDataPacket);
当连接按钮按下后,右侧的好友按钮才会可用:
(图4.2 客户端连接到服务器)
此时,发送消息的准备工作已经完成。点击要对话的好友按钮,发送按钮会变成可用,在下方的文本框里输入要发送的信息,点击发送按钮即可完成:
1 //按下好友按钮,发送按钮可用 2 B8889.addActionListener(new ActionListener(){ 3 @Override 4 public void actionPerformed(ActionEvent e) { 5 6 current = "8889"; 7 BSend.setEnabled(true); 8 } 9 10 });
其中,current为消息将要到达的目标端口号的标识,点击发送按钮,消息将会被发到current所表示的端口客户端。
发送按钮里的socket操作:
1 //建立数据报socket 2 InetAddress addr = InetAddress.getByName("localhost"); 3 DatagramSocket datagramSocket = new DatagramSocket(LocalPORTS); 4 5 //构造数据报并发送 6 String str = "T0" + current + "FR" + "8888" + ":" + tf.getText(); 7 DatagramPacket out = new DatagramPacket(str.getBytes(), str.getBytes().length, addr, ServerPORT); 8 datagramSocket.send(out); 9 10 //清空发送框 11 tf.setText(null); 12 13 //显示 14 String history = ta.getText(); 15 String now = String.format("%s%n", history) + "【" + df.format(new Date()) +"】" + "消息发送成功!"; 16 ta.setText(now); 17 //关闭数据报 18 datagramSocket.close();
演示,用客户端8888发送“网络交谈123”给客户端8889:
(图4.3 客户端8888发送消息成功)
(图4.4 客户端8889收到消息)
5.遇到的问题及解决过程
实践过程中碰到很多问题,持续时间很长,由于基础不牢,有时小问题都会花很长时间,绝大多数时间都花在解决问题上了。下面就过程中碰到的3个问题进行简要说明。
1)服务端点击启动后窗口无法正常关闭/启动客户端并点击连接按钮,窗口失效。
最开始是发现当服务端点击启动按钮运行后,窗口就不能点击右上角关闭了,需要用任务管理器才能关掉。之后在java课上,听老师讲到了阻塞的概念,才发现,DatagramSocket中receive函数在接受到数据报前一直处于阻塞状态,就是说,当没有收到消息时程序会停在这里,而这个时候,程序中其他部分就会处于不可用状态。知道了原因,但还是不知道怎样解决。在网上了解一番后,发现,将启动按钮中socket操作放在另外线程里能解决这个问题。就又去学习了线程的知识,才找到解决方案。
之后发现,启动客户端点击连接按钮后窗口上好友按钮显示可用,但是点击没有反应,界面底部的文本框也不能用,整个窗口像是卡住了一样。很快发应过来,这还是receive函数阻塞的原因,因为连接按钮的监听函数里,放了DatagramSocket的receive函数,需要总是处于等待消息状态。同样,加入线程后就解决了。
2)服务端未启动时,先启动客户端并点击连接按钮,需要将阻塞状态终止,显示连接超时消息。
很明显这也是receive阻塞,因为客户端的连接按钮的监听函数里通过DatagramSocket给服务器发送了请求连接报文,但是服务器未启动,不能返回确认连接报文,receive阻塞。这个情况也可以通过加入线程和设置定时器的方法可以解决,但是我通过查阅java API文档找到了更好的方法,发现可以给receive函数设置超时限制,既当receive函数在设置的时间里未能收到消息,会抛出SocketTimeoutException异常,通过捕获这个异常,就可以中断receive的阻塞,并可以显示超时信息。
1 datagramSocket.setSoTimeout(3000); //设置3秒超时
1 Try{ 2 DatagramSocket.receive(inDataPacket); 3 }catch(SocketTimeoutException e1) { 4 String history = ta.getText(); 5 String now; 6 if(!history.equals("")){ 7 now = String.format("%s%n", history) + "【" + df.format(new Date()) +"】" + "连接超时!"; 8 }else{ 9 now = "【" + df.format(new Date()) +"】" + "连接超时!"; 10 } 11 ta.setText(now); 12 datagramSocket.close(); 13 }
3)中文乱码
开始只能发送英文和数字,发中文就乱码,在网上查说是编码的问题,改了半天才发现我封装数据报的时候,字符串的长度计算不妥:
1 DatagramPacket out = new DatagramPacket(str.getBytes(),str.length(), addr, ServerPORT); //str为String类型
如果是英文或数字,str.length()计算的长度没问题,但是如果是汉字,就不对了,汉字不只占一个字节。
之后改成:
1 DatagramPacket out = new DatagramPacket(str.getBytes(), str.getBytes().length, addr, ServerPORT);
就可以了。
6. 存在的不足和接下来的工作
对数据消息的储存没有花太多心思,客户端只能显示最后一条离线消息。
这个只是在本机上模拟多客户端交谈,要想放在不同机器上,需要获得机器的ip地址,将DatagramSocket绑定到此地址即可。
图片的传输可以尝试。
7.完整代码
1 /*服务端*/ 2 import java.awt.Button; 3 import java.awt.Color; 4 import java.awt.Label; 5 import java.awt.TextArea; 6 import java.awt.event.ActionEvent; 7 import java.awt.event.ActionListener; 8 import javax.swing.JFrame; 9 import java.io.*; 10 import java.net.*; 11 import java.text.SimpleDateFormat; 12 import java.util.Date; 13 14 public class talkOnline_Server { 15 public static final int PORT =8081; 16 private static TextArea ta; 17 private static TextArea ta2; 18 static SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式 19 public static void main(String[] args) { 20 JFrame f = new JFrame(); 21 f.setLayout(null); 22 23 //连接按钮 24 Button BStart = new Button("启动"); 25 BStart.setBounds(10, 10, 50, 20); 26 27 //系统消息框标签 28 Label lb1 = new Label("--------系统消息框--------"); 29 lb1.setBounds(10, 55, 300, 20); 30 31 //系统消息框 32 ta = new TextArea(); 33 ta.setBackground(Color.gray); 34 ta.setEditable(false); 35 ta.setBounds(10, 80, 400, 400); 36 37 38 //当前用户列表标签 39 Label lb2 = new Label("--------当前用户列表--------"); 40 lb2.setBounds(450, 55, 300, 20); 41 42 //当前用户列表框 43 ta2 = new TextArea(); 44 ta2.setBackground(Color.gray); 45 ta2.setEditable(false); 46 ta2.setBounds(450, 80, 200, 400); 47 48 49 //将组件加入到框架 50 f.add(BStart); 51 f.add(lb1); 52 f.add(ta); 53 f.add(lb2); 54 f.add(ta2); 55 56 f.setSize(700,600); 57 f.setVisible(true); 58 f.setResizable(false); 59 f.setTitle("网络交谈_服务器"); 60 f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 61 62 BStart.addActionListener(new ActionListener() { //启动按钮 63 64 @Override 65 public void actionPerformed(ActionEvent e) { 66 thread t1 = new thread(); 67 t1.start(); 68 } 69 }); 70 71 } 72 73 static class thread extends Thread{ 74 public thread(){ 75 super(); 76 } 77 public void run(){ 78 InetAddress addr; 79 DatagramSocket ds = null; 80 String S1 = new String(), S2 = new String (), S3 = new String(); 81 try { 82 //建立数据报Socket并显示 83 addr = InetAddress.getByName("localhost"); 84 ds =new DatagramSocket(PORT,addr); 85 ta.setText("【" + df.format(new Date()) +"】" + "UDP服务器已启动:" + addr.getHostAddress() + "/" + addr.getHostName()); 86 87 //建立接受数据报 88 byte[] buf = new byte[1000]; 89 DatagramPacket inDataPacket = new DatagramPacket(buf, buf.length); 90 while(true){ 91 //等待数据报的到来 92 ds.receive(inDataPacket); 93 //数据的处理 94 String str = new String(inDataPacket.getData(), 0, inDataPacket.getLength()); 95 if(str.equals(new String("Request Connect"))){ //连接请求 96 String str2 = inDataPacket.getAddress() + "(" + inDataPacket.getPort() + ")"; 97 //更新用户列表 98 if(ta2.getText().indexOf(str2) == -1){ 99 String history2 = ta2.getText(); 100 String now2 = String.format("%s%n", history2) + str2; 101 ta2.setText(now2); 102 } 103 104 //返回确认连接报文 105 String strRep = "Connection Confirm"; 106 DatagramPacket outDataPacket = new DatagramPacket(strRep.getBytes(),strRep.length(), 107 inDataPacket.getAddress(), inDataPacket.getPort()); 108 ds.send(outDataPacket); 109 110 //更新系统消息列表 111 String history_SM = ta.getText(); 112 String now_SM = String.format("%s%n", history_SM) + "【" + df.format(new Date()) +"】" +str2 + "成功连接到服务器"; 113 ta.setText(now_SM); 114 115 if(!S1.isEmpty()){ //有8888的消息 116 117 outDataPacket = new DatagramPacket(S1.getBytes(),S1.getBytes().length, 118 addr, 8888); 119 ds.send(outDataPacket); 120 S1 = new String(""); 121 } 122 123 if(!S2.isEmpty()){ //有8889的消息 124 outDataPacket = new DatagramPacket(S2.getBytes(),S2.getBytes().length, 125 addr, 8889); 126 ds.send(outDataPacket); 127 S2 = new String(""); 128 } 129 if(!S3.isEmpty()){ //有8890的消息 130 outDataPacket = new DatagramPacket(S3.getBytes(),S3.getBytes().length, 131 addr, 8890); 132 ds.send(outDataPacket); 133 S3 = new String(""); 134 } 135 136 } 137 else{ 138 //消息处理 139 String Port = str.substring(2, 6); //收信方端口 140 if(ta2.getText().indexOf(Port) != -1){ //用户在线 141 String strRep = str; 142 DatagramPacket outDataPacket = new DatagramPacket(strRep.getBytes(),strRep.getBytes().length, 143 addr, Integer.parseInt(Port)); 144 ds.send(outDataPacket); 145 146 }else{ //用户不在线 147 if(Port.equals(new String("8888"))){ 148 S1 = str; 149 }else if(Port.equals(new String("8889"))){ 150 S2 = str; 151 }else{ 152 S3 = str; 153 } 154 } 155 String history = ta.getText(); 156 String now = String.format("%s%n", history) + "【" + df.format(new Date()) +"】" + str.subSequence(8, 12) + "给" + Port + "发送了一条消息。"; 157 ta.setText(now); 158 159 } 160 161 //.... 162 163 } 164 165 } catch (SocketException e1) { 166 // System.err.println("cannot open socket"); 167 ta.setText("cannot open socket"); 168 // System.exit(1); 169 } catch (IOException e1) { 170 // System.err.println("communication error"); 171 ta.setText("communication error"); 172 e1.printStackTrace(); 173 }finally{ 174 ds.close(); 175 } 176 177 } 178 } 179 180 } 181 182
1 /*客户端
请发表评论