原理
WebSocket protocol 。
现很多网站为了实现即时通讯,所用的技术都是轮询(polling)。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给客户端的浏览器。这种传统的HTTP request 的模式带来很明显的缺点 – 浏览器需要不断的向服务器发出请求,然而HTTP request 的header是非常长的,里面包含的有用数据可能只是一个很小的值,这样会占用很多的带宽。
而比较新的技术去做轮询的效果是Comet – 用了AJAX。但这种技术虽然可达到全双工通信,但依然需要发出请求。
在 WebSocket API,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。在此WebSocket 协议中,为我们实现即时服务带来了两大好处:
1. Header
互相沟通的Header是很小的-大概只有 2 Bytes
2. Server Push
服务器的推送,服务器不再被动的接收到浏览器的request之后才返回数据,而是在有新数据时就主动推送给浏览器。
一、项目简介
WebSocket是HTML5一种新的协议,它实现了浏览器与服务器全双工通信,这里就将使用WebSocket来开发网页聊天室,前端框架会使用AmazeUI,后台使用Java,编辑器使用UMEditor。
二、涉及知识点
网页前端(HTML+CSS+JS)和Java
三、软件环境 Tomcat 7 JDK 7 Eclipse JavaEE 现代浏览器
四、效果截图
效果1
效果2
五、项目实战
1. 新建项目
打开Eclipse JavaEE,新建一个名为Chat的Dynamic Web Project,然后导入处理JSON格式字符串所需要的包,把commons-beanutils-1.8.0.jar、commons-collections-3.2.1.jar、commons-lang-2.5.jar、commons-logging-1.1.1.jar、ezmorph-1.0.6.jar和json-lib-2.4-jdk15.jar这几个包放在WebContent/WEB-INF/lib目录下,最后把项目发布到Tomcat服务器上,到此空项目就搭建完成了。
2. 编写前端页面
在WebContent目录下新建一个名为index.jsp的页面,这里使用了AmazeUI框架,它是一个跨屏自适应的前端框架,消息输入框使用了UMEditor,它是一个富文本在线编辑器,能让我们的消息内容多姿多彩。
首先从 AmazeUI官网 下载压缩包,然后解压把assets文件夹拷贝到WebContent目录下,这样我们就能使用AmazeUI了。
再从 UEditer官网 下载Mini版的JSP版本压缩包,解压后把整个目录拷贝到WebContent目录下,接下来就可以编写前端代码了,代码如下(你可以按照自己的喜好编写):
?
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293 <%@ page language="java" contentType="text/htmlcharset=UTF-8" pageEncoding="UTF-8"%><!DOCTYPE html><html lang="zh"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"><title>ShiYanLou Chat</title><!-- Set render engine for 360 browser --><meta name="renderer" content="webkit"><!-- No Baidu Siteapp--><meta http-equiv="Cache-Control" content="no-siteapp" /><link rel="alternate icon" href="assets/i/favicon.ico"><link rel="stylesheet" href="assets/css/amazeui.min.css"><link rel="stylesheet" href="assets/css/app.css"><!-- umeditor css --><link href="umeditor/themes/default/css/umeditor.css" rel="stylesheet"><style>.title { text-align: center}.chat-content-container { height: 29rem overflow-y: scroll border: 1px solid silver}</style></head><body> <!-- title start --> <div class="title"> <div class="am-g am-g-fixed"> <div class="am-u-sm-12"> <h1 class="am-text-primary">ShiYanLou Chat</h1> </div> </div> </div> <!-- title end --> <!-- chat content start --> <div class="chat-content"> <div class="am-g am-g-fixed chat-content-container"> <div class="am-u-sm-12"> <ul id="message-list" class="am-comments-list am-comments-list-flip"></ul> </div> </div> </div> <!-- chat content start --> <!-- message input start --> <div class="message-input am-margin-top"> <div class="am-g am-g-fixed"> <div class="am-u-sm-12"> <form class="am-form"> <div class="am-form-group"> <script type="text/plain" id="myEditor" style="width: 100%height: 8rem"></script> </div> </form> </div> </div> <div class="am-g am-g-fixed am-margin-top"> <div class="am-u-sm-6"> <div id="message-input-nickname" class="am-input-group am-input-group-primary"> <span class="am-input-group-label"><i class="am-icon-user"></i></span> <input id="nickname" type="text" class="am-form-field" placeholder="Please enter nickname"/> </div> </div> <div class="am-u-sm-6"> <button id="send" type="button" class="am-btn am-btn-primary"> <i class="am-icon-send"></i>Send </button> </div> </div> </div> <!-- message input end --> <!--[if (gte IE 9)|!(IE)]><!--> <script src="assets/js/jquery.min.js"></script> <!--<![endif]--> <!--[if lte IE 8 ]> <script src="http://libs.baidu.com/jquery/1.11.1/jquery.min.js"></script> <![endif]--> <!-- umeditor js --> <script charset="utf-8" src="umeditor/umeditor.config.js"></script> <script charset="utf-8" src="umeditor/umeditor.min.js"></script> <script src="umeditor/lang/zh-cn/zh-cn.js"></script> <script> $(function() { // 初始化消息输入框 var um = UM.getEditor('myEditor') // 使昵称框获取焦点 $('#nickname')[0].focus() }) </script></body></html>
编写完成之后启动Tomcat服务器,然后访问 http://localhost:8080/Chat/index.jsp ,会看到如下界面。
3. 编写后台代码
新建一个com.shiyanlou.chat的包,在包中创建一个名为ChatServer的类,从JavaEE 7开始就统一了WebSocket的API,因此无论是什么服务器,用Java写的代码都是一样的,代码如下:
?
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950 package com.shiyanlou.chatimport java.text.SimpleDateFormatimport java.util.Dateimport javax.websocket.OnCloseimport javax.websocket.OnErrorimport javax.websocket.OnMessageimport javax.websocket.OnOpenimport javax.websocket.Sessionimport javax.websocket.server.ServerEndpointimport net.sf.json.JSONObject/** * 聊天服务器类 * @author shiyanlou * */@ServerEndpoint("/websocket")public class ChatServer { private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm") // 日期格式化 @OnOpen public void open(Session session) { // 添加初始化操作 } /** * 接受客户端的消息,并把消息发送给所有连接的会话 * @param message 客户端发来的消息 * @param session 客户端的会话 */ @OnMessage public void getMessage(String message, Session session) { // 把客户端的消息解析为JSON对象 JSONObject jsonObject = JSONObject.fromObject(message) // 在消息中添加发送日期 jsonObject.put("date", DATE_FORMAT.format(new Date())) // 把消息发送给所有连接的会话 for (Session openSession : session.getOpenSessions()) { // 添加本条消息是否为当前会话本身发的标志 jsonObject.put("isSelf", openSession.equals(session)) // 发送JSON格式的消息 openSession.getAsyncRemote().sendText(jsonObject.toString()) } } @OnClose public void close() { // 添加关闭会话时的操作 } @OnError public void error(Throwable t) { // 添加处理错误的操作 }}
4. 前后台交互
后台写完了,前台要用WebSocket连接后台,需要新建一个WebSocket对象,然后就可以和服务器端进行交互,从浏览器发送消息给服务器端,同时要验证输入框的内容是否为空,然后接受服务端发送的消息,把它们动态地添加到聊天内容框中,在
?
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354 var um = UM.getEditor('myEditor')$('#nickname')[0].focus()// 新建WebSocket对象,最后的/websocket对应服务器端的@ServerEndpoint("/websocket")var socket = new WebSocket('ws://${pageContext.request.getServerName()}:${pageContext.request.getServerPort()}${pageContext.request.contextPath}/websocket') // 处理服务器端发送的数据 socket.onmessage = function(event) { addMessage(event.data) } // 点击Send按钮时的操作 $('#send').on('click', function() { var nickname = $('#nickname').val() if (!um.hasContents()) { // 判断消息输入框是否为空 // 消息输入框获取焦点 um.focus() // 添加抖动效果 $('.edui-container').addClass('am-animation-shake') setTimeout("$('.edui-container').removeClass('am-animation-shake')", 1000) } else if (nickname == '') { // 判断昵称框是否为空 //昵称框获取焦点 $('#nickname')[0].focus() // 添加抖动效果 $('#message-input-nickname').addClass('am-animation-shake') setTimeout("$('#message-input-nickname').removeClass('am-animation-shake')", 1000) } else { // 发送消息 socket.send(JSON.stringify({ content : um.getContent(), nickname : nickname })) // 清空消息输入框 um.setContent('') // 消息输入框获取焦点 um.focus() } }) // 把消息添加到聊天内容中 function addMessage(message) { message = JSON.parse(message) var messageItem = '<li class="am-comment ' + (message.isSelf ? 'am-comment-flip' : 'am-comment') + '">' + '<a href="javascript:void(0)" ><img src="assets/images/' + (message.isSelf ? 'self.png' : 'others.jpg') + '" alt="" width="48" height="48"/></a>' + '<div><header><div>' + '<a href="javascript:void(0)">' + message.nickname + '</a><time>' + message.date + '</time></div></header>' + '<div>' + message.content + '</div></div></li>' $(messageItem).appendTo('#message-list') // 把滚动条滚动到底部 $(".chat-content-container").scrollTop($(".chat-content-container")[0].scrollHeight) }
到这步,简单的网页聊天室就完成了,你可以多开几个窗口或在局域网中邀请小伙伴们来一起测试。
六、小结
本次项目课使用WebSocket实现了简单的网页聊天室,其实WebSocket不仅可以应用于浏览器,也可以应用于桌面客户端。
package service_client_for_manyimport java.awt.BorderLayout
import java.awt.Font
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.io.IOException
import java.net.ServerSocket
import java.net.Socket
import java.util.Vector
import javax.swing.JButton
import javax.swing.JFrame
import javax.swing.JPanel
import javax.swing.JScrollPane
import javax.swing.JTextArea
import javax.swing.JTextField
/**
* 双工服务器,多人 本服务器默认不提供服务,而是在客户端连接时创建独立线程负责业务
**/
public class MutilServer implements ActionListener {
private JFrame frame
/** 边界布局的主面板 */
private JPanel panelMain
private JPanel panelDown
private JTextArea ta
private JTextField txt
private JButton but
private JScrollPane jsp
private Font font
/**
* 当前服务器使用端口
*/
private int port = 6666
/**
* 远程客户端的IP
*/
private String clientIp
/**
* 记录所有正在工作的服务员的登记表
*/
private Vector<Waiter>dengJiBiao
public MutilServer() {
frame = new JFrame("双工多人服务器")
panelMain = new JPanel(new BorderLayout())
panelDown = new JPanel()
ta = new JTextArea()
txt = new JTextField(20)
but = new JButton("发送")
jsp = new JScrollPane(ta)
// 粘贴界面
panelDown.add(txt)
panelDown.add(but)
panelMain.add(jsp, BorderLayout.CENTER)
panelMain.add(panelDown, BorderLayout.SOUTH)
// 字体
font = new Font("宋体", Font.BOLD, 18)
txt.setFont(font)
ta.setFont(font)
but.setFont(font)
// 文本域只读
ta.setEditable(false)
// 按钮添加监听
but.addActionListener(this)
frame.add(panelMain)
frame.setBounds(100, 300, 400, 400)
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)// 关闭窗体时结束程序
frame.setAlwaysOnTop(true)// 永远在所有窗体最上
frame.setVisible(true)
// 创建登记表
dengJiBiao = new Vector<Waiter>()
// 光标给消息文本框
txt.requestFocus()
createServer()
}
/**
* 显示文本到文本域,并且追加一个换行
*
* @param msg
*要显示的文本
*/
public void showTxt(String msg) {
ta.append(msg + "\n")
}
public static void main(String[] args) {
new MutilServer()
}
// 动作监听
public void actionPerformed(ActionEvent e) {
if (e.getSource() == but) {// 发送
txt.requestFocus()
String str = txt.getText().trim()
if(str.length()==0){
showTxt("不可以发送空消息")
return
}
if(dengJiBiao.size()==0){
showTxt("当前木有客户连接,无法发送信息")
return
}
str ="服务器消息:"+str
//找到所有登记表中的服务员,实现群发
for (int i = 0i <dengJiBiao.size()i++) {
Waiter w = dengJiBiao.get(i)//得到当前循环的服务员
w.send(str)
}
// 清空文本框,得到焦点
txt.setText("")
}
}
/**
* 启动网络服务器
*/
public void createServer() {
showTxt("正在启动服务器,使用本机" + port + "端口...")
try {
ServerSocket server = new ServerSocket(port)
showTxt("服务器启动成功,开始监听网络连接...")
while (true) {
Socket jiaoYi = server.accept()
// 每得到一个交易,就是来了一个客户端.需要交给一个新的服务员去维护处理
new Waiter(jiaoYi, dengJiBiao, this)
}
} catch (IOException e) {
showTxt("服务器启动失败,可能端口被占用")
}
}
}
package service_client_for_many
import java.awt.BorderLayout
import java.awt.Font
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.IOException
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.net.Socket
import java.net.UnknownHostException
import javax.swing.JButton
import javax.swing.JFrame
import javax.swing.JPanel
import javax.swing.JScrollPane
import javax.swing.JTextArea
import javax.swing.JTextField
/** 客户端双工 */
public class MyClient implements ActionListener{
private JFrame frame
/** 边界布局的主面板 */
private JPanel panelMain
private JPanel panelDown
private JTextArea ta
private JTextField txt
private JButton but
private JScrollPane jsp
private Font font
/**
* 服务器IP
*/
private String ip = "192.168.10.239"
/**
* 服务器端口
*/
private int port = 6666
private BufferedReader br
private BufferedWriter bw
public MyClient() {
frame = new JFrame("双工客户端1")
panelMain = new JPanel(new BorderLayout())
panelDown = new JPanel()
ta = new JTextArea()
txt = new JTextField(20)
but = new JButton("发送")
jsp = new JScrollPane(ta)
// 粘贴界面
panelDown.add(txt)
panelDown.add(but)
panelMain.add(jsp, BorderLayout.CENTER)
panelMain.add(panelDown, BorderLayout.SOUTH)
// 字体
font = new Font("宋体", Font.BOLD, 18)
txt.setFont(font)
ta.setFont(font)
but.setFont(font)
// 文本域只读
ta.setEditable(false)
//按钮添加监听
but.addActionListener(this)
frame.add(panelMain)
frame.setBounds(500, 200, 400, 400)
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)// 关闭窗体时结束程序
frame.setAlwaysOnTop(true)// 永远在所有窗体最上
frame.setVisible(true)
// 光标给消息文本框
txt.requestFocus()
linkServer()
}
/** 显示文本到文本域,并且追加一个换行
* @param msg 要显示的文本
*/
public void showTxt(String msg) {
ta.append(msg+"\n")
}
public static void main(String[] args) {
new MyClient()
}
//动作监听
public void actionPerformed(ActionEvent e) {
if (e.getSource() == but) {// 发送
if (bw == null) {
showTxt("当前没有客户端连接,无法发送消息")
return
}
String s = txt.getText().trim()// 得到文本框要发送的文字,去掉两端空格
if (s.length() == 0) {
showTxt("不可以发送空消息")
return
}
showTxt("我说:" + s)
try {
bw.write(s + "\n")// 发送网络消息给对方
bw.flush()// 清空缓冲
} catch (IOException e1) {
showTxt("信息:" + s + " 发送失败")
}
// 清空文本框,得到焦点
txt.setText("")
txt.requestFocus()
}
}
/**
* 连接服务器
*/
public void linkServer(){
showTxt("准备连接服务器"+ip+":"+port)
try {
Socket jiaoYi = new Socket(ip,port)
showTxt("连接服务器成功,开始进行通讯")
// 得到输入和输出
br = new BufferedReader(new InputStreamReader(
jiaoYi.getInputStream()))
bw = new BufferedWriter(new OutputStreamWriter(
jiaoYi.getOutputStream()))
String s = null
while ((s = br.readLine()) != null) {
showTxt( s)
}
} catch (UnknownHostException e) {
showTxt("连接服务器失败,网络连通错误")
} catch (IOException e) {
showTxt("与服务器通讯失败,已经断开连接")
}
showTxt("关闭")
}
}
package service_client_for_many
import java.awt.BorderLayout
import java.awt.Font
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.IOException
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.net.Socket
import java.net.UnknownHostException
import javax.swing.JButton
import javax.swing.JFrame
import javax.swing.JPanel
import javax.swing.JScrollPane
import javax.swing.JTextArea
import javax.swing.JTextField
/** 客户端双工 */
public class MyClient2 implements ActionListener{
private JFrame frame
/** 边界布局的主面板 */
private JPanel panelMain
private JPanel panelDown
private JTextArea ta
private JTextField txt
private JButton but
private JScrollPane jsp
private Font font
/**
* 服务器IP
*/
private String ip = "192.168.10.239"
/**
* 服务器端口
*/
private int port = 6666
private BufferedReader br
private BufferedWriter bw
public MyClient2() {
frame = new JFrame("双工客户端2")
panelMain = new JPanel(new BorderLayout())
panelDown = new JPanel()
ta = new JTextArea()
txt = new JTextField(20)
but = new JButton("发送")
jsp = new JScrollPane(ta)
// 粘贴界面
panelDown.add(txt)
panelDown.add(but)
panelMain.add(jsp, BorderLayout.CENTER)
panelMain.add(panelDown, BorderLayout.SOUTH)
// 字体
font = new Font("宋体", Font.BOLD, 18)
txt.setFont(font)
ta.setFont(font)
but.setFont(font)
// 文本域只读
ta.setEditable(false)
//按钮添加监听
but.addActionListener(this)
frame.add(panelMain)
frame.setBounds(900, 200, 400, 400)
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)// 关闭窗体时结束程序
frame.setAlwaysOnTop(true)// 永远在所有窗体最上
frame.setVisible(true)
// 光标给消息文本框
txt.requestFocus()
linkServer()
}
/** 显示文本到文本域,并且追加一个换行
* @param msg 要显示的文本
*/
public void showTxt(String msg) {
ta.append(msg+"\n")
}
public static void main(String[] args) {
new MyClient2()
}
//动作监听
public void actionPerformed(ActionEvent e) {
if (e.getSource() == but) {// 发送
if (bw == null) {
showTxt("当前没有客户端连接,无法发送消息")
return
}
String s = txt.getText().trim()// 得到文本框要发送的文字,去掉两端空格
if (s.length() == 0) {
showTxt("不可以发送空消息")
return
}
showTxt("我说:" + s)
try {
bw.write(s + "\n")// 发送网络消息给对方
bw.flush()// 清空缓冲
} catch (IOException e1) {
showTxt("信息:" + s + " 发送失败")
}
// 清空文本框,得到焦点
txt.setText("")
txt.requestFocus()
}
}
/**
* 连接服务器
*/
public void linkServer(){
showTxt("准备连接服务器"+ip+":"+port)
try {
Socket jiaoYi = new Socket(ip,port)
showTxt("连接服务器成功,开始进行通讯")
// 得到输入和输出
br = new BufferedReader(new InputStreamReader(
jiaoYi.getInputStream()))
bw = new BufferedWriter(new OutputStreamWriter(
jiaoYi.getOutputStream()))
String s = null
while ((s = br.readLine()) != null) {
showTxt(s)
}
} catch (UnknownHostException e) {
showTxt("连接服务器失败,网络连通错误")
} catch (IOException e) {
showTxt("与服务器通讯失败,已经断开连接")
}
showTxt("关闭")
}
}
package service_client_for_many
import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.IOException
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.net.Socket
import java.util.Vector
/**
* 服务员,线程类
* 在客户端连接后创建启动
* 负责当前客户端的所有数据收发
* 并且在业务需要时,将结果与服务器(老板)进行报告
*/
public class Waiter extends Thread{
private Socket sc
private Vector<Waiter>dengJiBiao
private MutilServer server
/**
* 客户端IP
*/
private String ip
private BufferedReader br
private BufferedWriter bw
/** 创建一个的新的服务员,负责当前传递的客户端连接(交易)
* 启动新线程
* @param sc 负责的交易
* @param dengJiBiao所有正在工作的服务员(所有交易)
* @param server 老板,也就是服务器
*/
public Waiter(Socket sc, Vector<Waiter>dengJiBiao,
MutilServer server) {
this.sc = sc
this.dengJiBiao = dengJiBiao
this.server = server
//初始化连接的准备工作
ip = sc.getInetAddress().getHostAddress()
// 得到输入和输出
try {
br = new BufferedReader(new InputStreamReader(
sc.getInputStream()))
bw = new BufferedWriter(new OutputStreamWriter(
sc.getOutputStream()))
} catch (IOException e) {
this.server.showTxt("与客户端:"+ip+"建立通讯失败")
e.printStackTrace()
return//无效客户,不再继续
}
this.server.showTxt("客户端:"+ip+"连接服务器成功")
//启动线程
this.start()
}
//线程
public void run(){
//开始时,登记开工
dengJiBiao.addElement(this)
System.out.println(this.getClass().getName())
try {
String s = null
while ((s = br.readLine()) != null) {
server.showTxt("客户"+ip+"说:" + s)
//当前客户发来的信息,其它客户也要能看得见.需要实现转发
//从登记表找到所有正在干活的服务员
for (int i = 0i <dengJiBiao.size()i++) {
Waiter w = dengJiBiao.get(i)
//排除掉当前服务员自己
if(w!=this)
w.send("客户"+ip+"说:" + s)
}
}
} catch (Exception e) {
server.showTxt("客户"+ip+"已经离开")
}
//结束时,登记下班
dengJiBiao.removeElement(this)
}
/** 发送信息给当前服务员负责的客户端
* @param msg
*/
public void send(String msg){
try {
bw.write(msg+"\n")
bw.flush()
} catch (Exception e) {
server.showTxt("给客户:"+ip+"发送信息"+msg+"失败")
}
}
}
一个服务器类·两个客户端类,一个线程类负责收发
欢迎分享,转载请注明来源:夏雨云
评论列表(0条)