资源下载地址:(23条消息) java实现网络聊天.rar-Java文档类资源-CSDN文库
WebSocket
WebSocket是什么?
-
WebSocket是一种网络通信协议。
什么是网络通信协议?
通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。
-
WebSocket是HTML5提供一种在单个TCP连接上进行全双工通讯的协议。
-
客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。
推送(PUSH)技术是一种建立在客户服务器上的机制,就是由服务器主动将信息发往客户端的技术。就像是广播电台播音。同传统的拉(PULL)技术相比,最主要的区别在于推送技术是由服务器主动向客户机发送信息,而拉技术则是由客户机主动请求信息。推送技术的优势在于信息的主动性和及时性。简单的说,相对于服务器,拉的技术是被动向客户端提供数据,推的技术是主动向客户端提供数据。
-
浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
为什么需要WebSocket?
传统HTTP协议:
HTTP协议是一种无状态的、无连接的、单向的应用层协议。
无状态的:无状态是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。即客户端发送HTTP请求后,服务器根据请求,会给我们发送数据,发送完后,不会记录信息。
无连接的:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接,采用这种方式可以节省传输时间。
单向的:它采用了请求/响应模型。通信请求只能由客户端发起,服务器端对请求做出应答处理。
这种通信模型有一个弊端:HTTP协议无法实现服务器主动向客户端发起消息,如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数Web应用程序将通过频繁的异步请求实现短连接、长轮询,轮询的效率低,非常浪费资源。
短连接:客户端和服务器每进行一次HTTP操作,就建立一次连接(进行三次握手),任务结束就中断连接。
长轮询:在长轮询机制中,客户端像传统轮询一样从服务器请求数据。然而,如果服务器没有可以立即返回给客户端的数据,则不会立刻返回一个空结果,而是保持这个请求等待数据到来或者恰当的超时,之后将数据作为结果返回给客户端。
![]()
WebSocket连接允许客户端和服务器之间进行全双工通信,以便任一方都可以通过建立的连接将数据推送到另一端。WebSocket只需要建立一次连接,就可以一直保持连接状态。它是一种长连接,只能通过一次请求来初始化连接,然后所有的请求和相应都是通过这个TCP连接进行通讯,这意味着它是一种基于事件驱动,异步的消息机制。这相比于轮询方式的不停建立连接显然效率要大大提高。
HTTP与WebSocket比较:
HTTP与WebSocket关系:
由于WebSocket协议使用HTTP协议进行握手和分手,一旦握手成功,中间是一个长连接。所以WebSocket协议不包含在HTTP协议里。
如何使用WebSocket?
-
WebSocket判断浏览器的兼容性,创建WebSocket连接对象(以下所有代码都是基于本系统的案例给出的)
if ('WebSocket' in window) {//WebSocket判断浏览器的兼容性 ws = new WebSocket(ws_url + "?loginName=" + loginUser);//创建WebSocket连接对象 } else if ('MozWebSocket' in window) {//火狐浏览器最新的WebSocket对象是MozWebSocket ws = new MozWebSocket(ws_url + "?loginName=" + loginUser); } else { console.log('Error: WebSocket is not supported by this browser.');//不支持 return; }
-
WebSocket编程遵循异步编程模式,也就是说,只要WebSocket连接打开,应用程序就简单地监听事件。客户端不需要主动轮询服务器得到更多的信息,要开始监听事件,只要为WebSocket对象添加回调函数。
和所有Web API一样,可以用on<事件名称>处理程序属性监听这些事件,WebSocket对象调度4个不同的事件:
-
open
一旦服务器响应了WebSocket连接请求,open事件触发并建立一个连接。open事件对应的回调函数称作onopen。
ws.onopen = function() { console.log('Info: WebSocket connection opened.'); };//连接成功触发事件
到open事件触发时,协议握手已经完成,WebSocket已经准备好发送和接收数据。如果应用程序接收到一个open事件,那么可以确定WebSocket服务器成功地处理了连接请求,并且同意与应用程序通信。
-
message
message事件在接收到消息时触发,对应于该事件的回调函数是onmessage。
ws.onmessage = function(message) { if ("string" == typeof (message.data)) {//传回来的是文字 console.log(message.data);//接收后端发来的信息,并展示在控制台 var receiveMsg = message.data; //$("#record").append("<div>" + receiveMsg + "</div>"); var obj = JSON.parse(receiveMsg);//JavaScript Object Notation---对象化字符串 if (obj.type == "s") {//如果是系统消息 $("#record").append( "<div>" + obj.msgDateStr + "</div><div>" + obj.msgSender + ": " + obj.msgInfo + "</div>"); var userHtml = ""; var userList = obj.userList; for (var i = 0; i < userList.length; i++) {//分行拼接字符串 userHtml = userHtml + userList[i] + "<br/><br/>"; } $("#userList").html(userHtml); } else if (obj.type == "p") {//若是个人发的消息 $("#record").append( "<div>" + obj.msgDateStr + "</div><div>" + obj.msgSender + ": " + obj.msgInfo + "</div>"); } else if (obj.type == "img") {//若是发的图片 picp = "<div>" + obj.msgSender + ": " + obj.msgDateStr + "</div>"; } else { //未来可以用于扩展功能 } } else {//传回来的是图片 var reader = new FileReader(); reader.readAsDataURL(message.data); reader.onload = function(evt) { if (evt.target.readyState == FileReader.DONE) {//判断传输是否正常 var url = evt.target.result; $("#record") .append( picp + "<div><img src='"+url+"' style='max-height:150px; max-width:150px; vertical-align: middle; align: center;'/></div>"); } } } };//通讯时触发事件,用message来接收 };
-
error
error事件在响应意外故障的时候触发,与该事件对应的回调函数为onerror。错误还会导致WebSocket连接关闭。如果你接收一个error事件,可以预期很快就会触发close事件。close事件中的代码和原因有时候能告诉你错误的根源。error事件处理程序是调用服务器重连逻辑以及处理来自WebSocket对象的异常的最佳场所。
ws.onerror = function() { console.log("WebSocket连接发生错误"); };//连接发生错误的回调方法
-
close
close事件在WebSocket连接关闭时触发,这可能有多种原因,比如连接失败或者成功的WebSocket关闭握手。对应于close事件的回调函数是onclose,一旦连接关闭,客户端和服务器不再能接收或者发送消息。
ws.onclose = function() { console.log('Info: WebSocket closed.'); };//连接关闭触发事件
-
后台支持代码如下:
@ServerEndpoint(value = "/chatroom") // 具体的请求名,与前端对应 public class WSServPoint { static Map<Session, String> us = new HashMap<Session, String>();// session和对象用户名对应起来 // 创建session集合用来收集多人聊天的session对象 // 必须使用静态的,否则没办法共享,因为普通的成员变量只属于本对象 Map<String, String> map; private Msg ms; @OnOpen public void onOpen(Session session) throws UnsupportedEncodingException { System.out.println("连接建立成功!"); String msg = session.getQueryString();// 获取前台登录的用户信息 msg = URLDecoder.decode(msg, "utf-8");// 乱码,重新编码 // 拆分字符串 map = new HashMap<String, String>(); if (msg.contains("&")) { String[] sts = msg.split("\\&"); for (String str : sts) { String[] strs = str.split("="); map.put(strs[0], strs[1]); } } else { String[] strs = msg.split("="); map.put(strs[0], strs[1]); } // userList.add(map.get("loginName")); System.out.println("map:" + map); us.put(session, map.get("loginName")); ms = new Msg(); ms.setType("s"); ms.setMsgSender("system");// 系统发送信息 ms.setMsgDate(new Date()); ms.setUserList(new ArrayList<String>(us.values()));// us中键值对的值表示用户名称 ms.setMsgInfo(map.get("loginName") + "已上线!"); String jsonStr = JSONObject.toJSONString(ms); // System.out.println("msg:" + msg); // set.add(session);// 收集session对象 bordcast(us.keySet(), jsonStr); } @OnClose public void onClose(Session session) { // userList.remove(map.get("loginName")); us.remove(session); ms = new Msg(); ms.setType("s"); ms.setMsgSender("system"); ms.setMsgDate(new Date()); ms.setUserList(new ArrayList<String>(us.values())); ms.setMsgInfo(map.get("loginName") + "已下线!!"); String jsonStr = JSONObject.toJSONString(ms); // set.remove(session); bordcast(us.keySet(), jsonStr); System.out.println("连接已关闭!!!"); } @OnMessage public void onMessage(String message, Session session) throws IOException { System.out.println("信息已接收!" + message); Msg ms = new Msg(); ms.setType("p"); ms.setMsgSender(map.get("loginName")); ms.setMsgDate(new Date()); if (message.startsWith("@") && message.contains(":")) {// 私聊判断,是否已@开头,其中包含:代表着私聊 String reivName = message.substring(message.indexOf("@") + 1, message.indexOf(":"));// 取出私聊人 if (us.containsValue(reivName)) {// 判断us是否有这个人 for (Entry<Session, String> e : us.entrySet()) {// 遍历us,拿到session if (reivName.equals(e.getValue())) { Session reivSession = e.getKey(); message = message.substring(message.indexOf(":") + 1);// 发送的消息在:之后 ms.setMsgInfo("私信->" + reivName + "" + message); ms.setMsgReceiver(reivName); String jsonstr = JSONObject.toJSONString(ms); Set<Session> hashSet = new HashSet<Session>(); hashSet.add(reivSession);// 发送者收到 hashSet.add(session);// 自己也要显示 bordcast(hashSet, jsonstr); break; } } } else { ms.setMsgInfo(message); String jsonstr = JSONObject.toJSONString(ms); bordcast(us.keySet(), jsonstr); } } else { ms.setMsgInfo(message); String jsonstr = JSONObject.toJSONString(ms); bordcast(us.keySet(), jsonstr); } } byte[] bc = null; @OnMessage public void onMessage(byte[] input, Session session, boolean flag) { if (!flag) {// 传输图片数据不一定一次性传输完成,可能会分割成几次,因此会多次调用该函数 bc = ArrayUtils.addAll(bc, input);// 将字节数组集中整合在一个数组 } else { bc = ArrayUtils.addAll(bc, input); ByteBuffer bb = ByteBuffer.wrap(bc); bc = null;// 清空 Msg ms = new Msg(); ms.setMsgSender(map.get("loginName")); ms.setMsgDate(new Date()); ms.setType("img"); String jsonstr = JSONObject.toJSONString(ms); bordcast(us.keySet(), jsonstr); bordcast(us.keySet(), bb); } } @OnError public void onError(Throwable t) throws Throwable { System.out.println("系统异常!"); } public void bordcast(Set<Session> set, String message) {// 广播,将信息回传给前端 for (Session s : set) { try { s.getBasicRemote().sendText(message); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public void bordcast(Set<Session> set, ByteBuffer bb) {// 传输图片,二进制数据流 for (Session s : set) { try { s.getBasicRemote().sendBinary(bb); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
Socket
-
Socket并不是一个协议,而是抽象出来的一层,应用于应用层和传输控制层之间的一组接口
-
基于TCP协议的通信
TCP协议:
使用TCP协议前,须先建立TCP连接,形成传输数据通道
传输前,采用“三次握手”方式,是可靠的
TCP协议进行通信的两个应用进程:客户端、服务端
传输完毕,需释放已建立的连接,效率低
-
通信的两端都要有Socket,是两台机器间通信的端点
-
网络通信其实就是Socket间的通信
-
Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输
-
一般主动发起通信的应用程序属客户端,等待通信请求的为服务端
作为客户端代码:
class MySend1 { public MySend1() throws Exception { Socket socket = new Socket(InetAddress.getByName(MyEvent1.myIp), 5000);//创建客户端,需要服务器的IP地址和接收端口号 Calendar c = Calendar.getInstance();//时间 byte[] buf = ("from IP:" + InetAddress.getLocalHost() + "\r\n" + MyEvent1.myText).getBytes();//封装消息 //发送消息 OutputStream os = socket.getOutputStream(); os.write(buf); os.flush();//将输入流和输出流中的缓冲进行刷新,使缓冲区中的元素即时做输入和输出,而不必等缓冲区满 socket.shutdownOutput();//告知服务器输出结束 //界面展示 String date = "Time:" + c.get(Calendar.YEAR) + "年" + (c.get(Calendar.MONTH) + 1) + "月" + c.get(Calendar.DAY_OF_MONTH) + "日" + c.get(Calendar.HOUR) + "时" + c.get(Calendar.MINUTE) + "分"; MyFrame1.receField.append(date + "\r\nME:" + MyEvent1.myText + "\r\n"); //关流 os.close(); socket.close(); } }
作为服务端代码:
class MyRecevier1 extends Thread{ Calendar c; public MyRecevier1() { c = Calendar.getInstance();// 获取当前时间 } @Override public void run() {// 线程进入运行状态 ServerSocket socket = null;//创建一个服务端 Socket accept = null;//接收客户端请求 String ss = null;//消息内容 try { socket = new ServerSocket(5050);//端口需要和发送端相同 while(true){ accept = socket.accept(); InputStream is = accept.getInputStream();//获取一个输入流,读取客户端发送的数据 //编辑信息 int len = 0; byte[] buf = new byte[1024]; while ((len=is.read(buf))!=-1){ ss = new String(buf,0,len); } String date = "Time:" + c.get(Calendar.YEAR) + "年" + (c.get(Calendar.MONTH) + 1) + "月" + c.get(Calendar.DAY_OF_MONTH) + "日" + c.get(Calendar.HOUR) + "时" + c.get(Calendar.MINUTE) + "分"; MyFrame1.receField.append(date + "\r\n" + ss + "\r\n");//展示 } } catch (IOException e1) { e1.printStackTrace(); }; } }
DatagramSocket
-
基于UDP协议的通信
将数据、源、目的封装成数据包,不需要建立连接
因无需连接,故是不可靠的
发送数据结束时无需释放资源,速度快
-
类 DatagramSocket 和 DatagramPacket 实现了基于 UDP 协议网络程序
-
UDP数据报通过数据报套接字 DatagramSocket 发送和接收,系统不保证UDP数据报一定能够安全送到目的地,也不能确定什么时候可以抵达
-
DatagramPacket 对象封装了UDP数据报,在数据报中包含了发送端的IP地址和端口号以及接收端的IP地址和端口号
-
UDP协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和接收方的连接
实现通讯的代码:
class MySend { public MySend() throws Exception { DatagramSocket dSocket = new DatagramSocket(10000);//构建一个数据报套接字绑定到本地主机的10000端口 Calendar c = Calendar.getInstance(); byte[] buf = ("from IP:" + InetAddress.getLocalHost() + "\r\n" + MyEvent.myText) .getBytes(); System.out.println("收到内容:" + MyEvent.myText); DatagramPacket dPacket = new DatagramPacket(buf, buf.length, InetAddress.getByName(MyEvent.myIp), 8888);// 自定义,接收端的IP地址和端口号 dSocket.send(dPacket);//从这个套接字发送数据报包,DatagramPacket包括信息显示要发送的数据长度,远程主机的IP地址和端口号,远程主机。 dSocket.close(); String date = "Time:" + c.get(Calendar.YEAR) + "年" + (c.get(Calendar.MONTH) + 1) + "月" + c.get(Calendar.DAY_OF_MONTH) + "日" + c.get(Calendar.HOUR) + "时" + c.get(Calendar.MINUTE) + "分"; MyFrame.receField.append(date + "\r\nME:" + MyEvent.myText + "\r\n"); } } class MyRecevier implements Runnable { Calendar c; public MyRecevier() { c = Calendar.getInstance();//获取当前时间 } @SuppressWarnings("resource") @Override public void run() {//线程进入运行状态 DatagramSocket dSocket = null;//DatagramSocket使用的UDP连接,客户端不需要先连接数据,可以直接发送给指定服务端 try { dSocket = new DatagramSocket(8888);//构建一个数据报套接字绑定到本地主机的8888端口 } catch (SocketException e1) { e1.printStackTrace(); } ; byte[] buf = new byte[1024]; DatagramPacket dPacket = new DatagramPacket(buf, buf.length);//接收数据包长度 length DatagramPacket构建 while (true) { try { dSocket.receive(dPacket);//接收数据报包从这个插座 } catch (IOException e) { e.printStackTrace(); } String string = new String(dPacket.getData(), 0, dPacket.getLength());//构建了一种新的 String通过解码指定的字节数组使用平台的默认字符集 String date = "Time:" + c.get(Calendar.YEAR) + "年" + (c.get(Calendar.MONTH) + 1) + "月" + c.get(Calendar.DAY_OF_MONTH) + "日" + c.get(Calendar.HOUR) + "时" + c.get(Calendar.MINUTE) + "分"; MyFrame.receField.append(date + "\r\n" + string + "\r\n"); System.out.println("string:" + string); } // dSocket.close(); } }