开发者学堂课程【高校精品课-上海交通大学-企业级应用体系架构:WebSocket 】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/75/detail/15830
WebSocket
内容介绍:
一、WebSocket
二、Introduction to WebSocket
三、Creating WebSocket Applications
四、Creating and Deploying a WebSocket Endpoint
跑 zookeeper 用的是 Kafka中quickstart 写的启动服务的方法进行启动,跑 zookeeper 和跑 kafka,会有两个窗口,一个跑 kafka,一个跑 zookeeper,写发送者和接受者的代码,逻辑很简单,发送者连接到 localhost:9092,kafka服务器上,所以里面会有服务器的位置,下面可以看到kafkaproducer生产表,string,string 文件里面用字符串描述,直接发出100条消息,发到kafka的topic test中,Integer. toString(i), value: "Message‘’+ Integer, toString i循环变量,结束。接收者也要连接到服务器上,
while (true) {
Consume rRecords<string, String> records =consumer. poll(Duration. ofMillis(100));
不断的拿消息
for (ConsumerRecord<String, String> record : records )
offset 是什么,输入输出分别是什么,消费者先跑起来,死循环在注册等待,发送者发消息后退出,接收者在接收消息,offset 从101开始是因为已经执行过一遍,消息不会马上删除,而是存活一段时间,在这段时间里面,没有死掉,再发消息就是从101开始到200,收到的消息内容是从0到99,这就是 kafka 的例子,利用 kafka 实现,当消息通信想用重量级的,里面包含太多不需要用的功能,就考虑用 kafka 实现,代码中 service 层收发消息即可,相比之下,从编程代码可以看到,做一定的封装,代码比原生的 jms 代码写起来要少一些。
一、WebSocket
有哪些通信的方式,在系统里面如何选择?
WebSocket is an application protocol that provides full-duplex
communications between two peers over the TCP protocol.想实现全双功的通信
1、In the traditional request-response model used in HTTP 客户端发请求服务, the client requests resources and the server provides responses.适当给响应,唤醒时候有响应,很快速的响应,服务器端能不能主动把消息推出去,service处理客户端请求时,接收到一个消息把它放到一个 q 里面,后端进行处理,处理完之后监听另外一个 q,处理结果,处理完把结果放到另外一个 topic 里面,topic 会被监听到,发生在服务器端,如何把最终消息推给客户端,在http协议里面必须要先有请求,才有响应,现在客户端没有发请求,需要双功式,不但客户端发请求,服务端也能主动推消息,双向通信,像 jms 中的点对点一样,现在的客户端和服务器端的模型有点模糊,双方之间必须都能通起来。WebSocket 是通过 http 协议走到它的应用层面,在 tcp 的协议上和 http 有差异,应用层不是最新的一次协议,所以它是新写的,所以 ws://,http://,有主动推送消息的方式,可以看到系统变得灵活,服务系统主动推
2、The exchange is always initiated by the client; the server cannot send any data without the client requesting it first.
3、The WebSocket protocol provides a full-duplex communication channel between the client and the server.
4、Combined with other client technologies, such as JavaScript and HTML5, WebSocket enables web applications to deliver a richer user experience.
二、Introduction to WebSocket
1、In a WebSocket application, the server publishes a
WebSocket endpoint and the client uses the endpoint's URI to
connect to the server.在 WebSocket 中服务器需要发布 endpoint,相当于在 http 中看到 service 的作用一样,要能接收客户端浏览器发过来的连接请求,要想收到主动推的消息,必须要进行注册,一旦注册就知道在监听消息,未来就可以推消息,利用 uri 连接服务器,服务器发布一个 endpoint,实际上有 uri,客户端对 uri 要监听消息,服务器端收到请求以后,要把消息推过去。
The WebSocket protocol is symmetrical after the connection has been established:
(1)The client and the server can send messages to each other at any time while
the connection is open, and they can close the connection at any time.
(2)Clients usually connect only to one server, and servers accept connections from multiple clients.
2、The WebSocket protocol has two parts:
handshake and data transfer.
握手和传数据,握手指的是大家如何建立连接,一旦建立连接后,WebSocket是对称的作用,建立连接之后,客户端和服务器端都能用连接进行发送,或者接收消息,大家在看待这个连接,并不想在http中只能是客户端发请求给服务器端响应,服务器端发请求给客户端进行接收,所以是分工的。
3、The client initiates the handshake by sending a request to a
WebSocket endpoint using its URI.
建立连接
The handshake is compatible with existing HTTP-based infrastructure:
web servers interpret it as an HTTP connection upgrade request.
An example handshake from a client looks like this:
GET /path/ to/websocket/ endpoint HTTP/1.1
客户端通过 get 方法对 endpoint uri 发送
Host: localhost 机器主机的域名,唯一的标识
Upgrade: websocket 明发送的属性
Connection: Upgrade 升级版链接
Sec -WebSocket- Key: xqBt3ImNzJbYqRINxEFlkg==发过去,服务器端收到包,收到后服务器端会发一个响应给客户端
Origin: http://localhost
Sec -WebSocket-Version: 13
Sec -WebSocket - Accept:K7DJLdLooIwIG/ MOpvWFB3y3FE8=
4、The server applies a known operation to
the value of the Sec -WebSocket -Key header to generate the value of the Sec-WebSocket- Accept header.
服务器端拿到发过来的key后,双方都解的操作,比如约定的操作,浏览器默认的操作
5、The client applies the same operation to 服务器端发回给客户端,客户端用同样的操作解 Sec-WebSocket- Key,用 key 解,如果解开,就是 key 算出来,双方建立一次连接,相当于鉴别身份,但是安全性比较低,只是确认一下能否握手,一旦解开,对 key 产生相同的操作,看 accept 是否一致,如果一致相匹配,互相之间建立的一个连接。
the value of the Sec-WebSocket- Key header, and the connection is established successfully if the result matches the value received from the server.
6、The client and the server can send messages to each other after a successful handshake.
7、WebSocket endpoints are represented by URIs that have the
following form:
Ws://host: port/path?query发,比如握手,利用协议发,不是 http 也不是 https,其次跟它们很像,但是实际上不是 http 协议,看浏览器和应用服务器是否支持 websocket,虽然支持的越来越多,不支持的占少数,区别和http和https 一样,加密
WSS:/ /host: port/ path?query
The WS scheme represents an unencrypted WebSocket connection, and the WSS scheme represents an encrypted connection.
The port component is optional;
the default port number is 80 for unencrypted connections and 443 for encrypted connections.默认端口
The path component indicates the location of an endpoint within a server.
The query component is optional.
建立好连接之后,刷数据,所有的东西都要经过连接。
三、Creating WebSocket Applications
1、The Java API for WebSocket consists of the following packages:
The javax . websocket . server package contains annotations, classes, and interfaces to create and configure server endpoints.相应的包进行导入
The javax . websocket package contains annotations, classes, interfaces,and exceptions that are common to client and server endpoints.
2、WebSocket endpoints are instances of the javax .websocket . Endpoint class.服务器端
The Java API for WebSocket enables you to create two kinds of endpoints:
programmatic endpoints and annotated endpoints.两种方法
To create a programmatic endpoint, you extend the Endpoint class and override its lifecycle methods.
To create an annotated endpoint, you decorate a Java class and some of its methods with the annotations provided by the packages above.
After you have created an endpoint, you deploy it to an specific URI in the application so remote clients can connect to it.
四、Creating and Deploying a WebSocket Endpoint
1、The process for creating and deploying a WebSocket endpoint:
Create an endpoint class.
Implement the lifecycle methods of the endpoint.
Add your business logic to the endpoint.
Deploy the endpoint inside a web application.
The process is slightlý different for programmatic endpoints
and annotated endpoints编程方式写
2、Programmatic Endpoints
EchoEndpoint
类
,
子类
,
里面有若干个方法写
public class EchoEndpoint extends Endpoint
{
@Override
public void onOpen(final Session session,EndpointConfig config)
有客户端浏览器发送一个请求过来,连接时,握手要干什么,
支持 websocket 外部浏览器,包装好的东西,包含用户的信息,在什么位置等待,uri 会配合什么,要处理的就是如何对待 session
{
session . addMessageHandler(
双功的,相当于发个消息,消息处理器,不能是同步处理,是异步处理,当客户端有事情过来的时候就要去处理
new MessageHandler . Whole<String>()
{
@Override
public void onMessage(String msg)
{
try [
session. getBasicRemote( ). sendText(msg);
}
catch(IOException e)
{..
.
}
}
}
);
}
针对用户加一个消息处理器,一旦有消息过来,就要得到远程客户端,
可以理解 为session. getBasicRemote 就是远程的客户端,session 就是把它包装起来的样子,对远程客户发消息,逻辑是业务逻辑,如何写都可以,含义是只要有人连接,就在上面注册一个监听器,监听器未来在session上一旦有消息过来,原封不动的把消息转发出去,用户的消息再发送出去。代码量比较多,而且不是很明确,编程的模式不是特别在意。
To deploy this programmatic endpoint, use the following code
in your Java EE application:
用复解的方式做,还需要部署一下,可以看到类的静态方法,类的一个实例,把它部署到 echo 位置上。
ServerEndpointConfig . Builder. create( EchoEndpoint . class,
"/echo"). build();
When you deploy your application, the endpoint is available
atws:/ /<host>: <port>/ <application>/ echo;
for example, ws://localhost: 8080/ echoapp/ echo.
未来是对应的 url,协议,主机名端口,应用,配的路径,未来客户端访问对 ws 进行操作
3、Annotated Endpoints
EchoEndpoint
@ServerEndpoint(" /echo" )
标注
public class EchoEndpoint
{
扩展类或者实现接口
@OnMessage
public void onMessage(Session session, String msg)
{
发过来之后就会有session信息
try
{
session. getBasicRemote(). sendText(msg);
谁发消息都原封不动的传回去
}
catch (IOException e)
{
...
}
}
}
代码看起来简单的多,并且非常的直观。
Annotation |
Event |
Example |
OnOpen |
Connection opened.连接被打开,当有人发消息时用握手的方式建立连接 |
@OnOpen public void open(Session session,EndpointConfig conf){}名字可以随便写 |
OnMessage |
Message received.接收消息之后用什么方法处理 |
@OnMessage public void message (Session session, String msg) {} |
OnError |
Connection error.一旦调用出错,调用哪个处理方法 |
@OnError public void error(Session session, Throwable error){} |
OnClose |
Connection closed.把连接关闭掉时应该干什么 |
@OnClose public void close(Session session, CloseReason reason)「」 |
4、Sending Messages to All Peers Connected to an Endpoint
Send messages
@ServerEndpoint("/echoall")
public class EchoAllEndpoint
{从名字可以看出把消息发送给所有的用户,未来不管是谁发消息,都要遍历当前的实例,维护所有的 session,就是获取当前所有保持连接的 session,比如设计股票操作的应用,很多人都连着,针对里面每一个人,for 循环,如果是开着的就把消息发出去,之后不管谁发消息都把消息发回给保持连接的封装,就是向所有人保持回复。
@OnMessage
public void onMessage(Session session, String msg)
{
try {
for (Session sess : session. getopenSessions()) {
引入一个概念,通过方法的调用,可以跟 endpoint 连接的所有的客户端,比如在线聊天室等方法,应用程序需要应用方法,让聊天室里的人都知道消息,用的是 for 循环做处理
if (sess. is0pen())如果逻辑写的比较复杂,保持开放的人很多,那就会遇到执行时间非常长的问题,所以逻辑不易复杂,简单的进行处理即可,如果过长,大家频繁的发消息,逻辑执行非常长,会处理不过来
sess. getBasicRemote() . sendText(msg);
}
} catch ( IOException e) { ... }
}
5、Receive messages 接收消息
@ServerEndpoint(" /receive")
public class ReceiveEndpoint (
@OnMessage发过来的message不一定是字符串文本消息,还有可能是字节数据,甚至还有可能是对象,实际上可以处理各种各样不同类型的消息,所以 OnMessage 不 一定只有一个,可以专门去处理不同的消息
public void textMessage(Session session, String msg)
{
System. out. println("Text message: " + msg);
}
@OnMessage
public void binaryMessage(Session session, ByteBuffer msg )
{
System. out. println("Binary message:
" + msg.toString());
}
@OnMessage
public void pongMessage( Session session, PongMessage msg)
{
System. out . println("Pong message:
+
msg. getApplicationData(). toString());
}
}
6、An example
(1)ETFEndPoint.java
@ServerEndpoint(" /dukeetf")
public class ETFEndpoint {
private static final Logger logger =
Logger . getLogger("ETFEndpoint");
static Queue<Session> queue = new
ConcurrentLinkedQueue<>();
定义
,
未来是并发链接的
queue,
当有大量用户创建连接时可以同时塞进
queue
中
,
queue
里面存放的是很多个 session
,
当有大量 session 建立连接都希望把自己注册上来时
,
可以并发的写入
queue
中
public static void send(double price, int volume) {
定义方法
,
没有任何注解
,
是自己写的
,
给
double
类型的价格
,
int 类型数量会组一个字符串
,
把字符串当成消息
String msg = String. format("%.2f, %d", price, volume);
try {
for (Session session : queue) {
对于
queue
中所有的 session
session. getBasicRemote( ). sendText(msg);
都要获取它们的客户端
,
发送文本消息出去
logger .log(Level. INFO, "Sent:
(0", msg);
}
} catch (IOException e) {
logger . log(Level. INFO, e. toString());
}
只要 send 给数据,就把数据推给所有的客户端。
(2)ETFEndPoint.java
@OnOpen
public void openConnection(Session session) {
queue . add(session);
logger .log(Level. INFO, "Connection opened.");
}
@OnClose
public void closedConnection(Session session) {
queue . remove(session) ;
logger. log(Level. INFO, "Connection closed.");
}
@OnError
public void error(Session session, Throwable t) {
queue. remove( session);
logger. log(Level. INFO, t. toString());
logger .log(Level. INFO, "Connection error.");
}
}
当有人连接时把 session 添加到队列中,关闭时,再从队列中把 session 移除掉,session error,把它移除掉,没有特别的逻辑,用日志说明一下。
(3)ETFListener.java
@WebListener
定义监听器
public class
ETFListener implements ServletContextListener {
private Timer timer = null ;
public void contextInitialized( ServletContextEvent event ) {
timer = new Timer(true);
event . getServletContext( ).log("The Timer is started");
timer . schedule( new ReportBean( event .
getServletContext()) ,
0, 1000);
event. getServletContext().log("The task is added");
}
}
定义一个定时器,定时器每一秒钟动作一次,创建一个 ReportBean 实例。
(4)ReportBean.java
public class ReportBean extends TimerTask {
定时器任务
,
跟定时关联起来
,
每一秒都执行一个
private ServletContext context = null ;
private Random random = new Random( ) ;
private double price = 100.0;
private int volume = 300000;
public ReportBean( ServletContext context )
{ this. context = context; }
public void run() {
只要时间到了
,
都会生成价格
,
生成 volume
,
调用方法发出去
context.log("Task started");
price += 1. 0*(random . nextInt(100)-50)/100.0;
volume += random. nextInt(5000) - 2500;
ETFEndpoint. send(price, volume) ;
context.log( "Task ended");
}
}
逻辑每隔一秒用随机事务生成价格数量,用websocket推出去。
(5)Index.html客户端是一个脚本,支持websocket浏览器里面,内置一个wsocket对象,在创建的时候告诉它对着谁,在本地跑应用
<html>
<head>
<title>Duke's WebSocket ETF</title>
<script type="text/javascript">
var wsocket ;
function connect() {
wsocket = new WebSocket
("ws: //localhost : 8080/WebSocketSamples/ dukeetf");主机名,业务名,endpoint,建立连接,发出请求之后相当于 onopen 方法被调用,客户端添加到 queue 中
wsocket. onmessage = onMessage;一旦有消息回来,调用 onmessage 方法
}
function onMessage(evt) {
var arraypv = evt.data.split(",");
document . getElementById(" price") . innerHTML = arraypv[0];
document . getElementById( "volume") . innerHTML = arraypv[1];
}
window . addEventListener("load", connect, false);
</script>
</head>
发过来的是价格和 volume,价格显示两位小数,中间加逗号,数量,显示成十进制数。用逗号隔开,放到数组里前面是价格,后面是数量,拿价格掉替换页面里面的 arraypv,数量替换到页面的 volume,window 把监听器进行注册,在整个页面加载时跑 connect,每次有消息过来就进行替换,每次有消息都会进行替换。
Index.html
<body>
<h1>Duke's WebSocket ETF</h1>
<table>
<tr>
<td width= "100">Ticker</td>
<td align="center">Price</td>
<td
id="price '
style="font-s ize :24pt ; font-weight :bold;">--.--</td>
内容进行替换
</tr>
<tr>
<td style="font-size:18pt ; font-weight:bold; "
width="100">DKEJ</td>
<td align="center" >Volume</td>
<td id="volume" align="right">--</td>
内容进行替换
</tr>
</table>
</ body>
< /html>
跑服务器端,可以看到一秒接收一次,两个浏览器同时在变,说明两个浏览器同时注册到 websocket服务器上,websocket 推消息,同时收到,现在浏览器上什么都没做,浏览器里面的页面也没有发消息出去,数字的变化完全是服务器端推出来的,这是 websocket 的作用。
rmi 做同步调用,jms 做异步调用,都发生在后台,java 代码之间,如果前台和后台交互,使用 ajax,现在有websocket,系统里都会有吗?
rmi 是在后台 server 端,有两个进程,它俩之间的通信必须要用 rmi,因为它俩之间没有办法直接进行交互,比如后台做集群,直接让集群上两个进程通信,zookeeper 和 kafka 之间通信也需要找 rmi,虽然 rmi 是后台的,多个之间进程的通信,必须要这样访问,jms本质上 也是不同进程之间的通信,只不过中间有 rmi 服务器的中介,解决的是rmi的直接通信的饱和度太高,再进行调用之后一旦出错,任务就失败了,没有重新尝试的机会,再就是要继续堵塞等待,所以希望以异步的方式进行通信,所以有 jms,这些发生在服务器里面,服务器和客户端之间,price 功能就是不断接收后台发过来数据,可以用 ajax 做,ajax 进行周期性的轮询,到后面去问有没有新数据,有就给,极大的缺点就是造成后台的压力很大,也许数据很长时间变一次,去抓一下,每隔一秒去抓一下,压力很大。也有可能一秒钟要刷好几次,一秒刷一次又太慢。
所以前后台匹配不好,这种情况下不太适合,ajax 主要希望页面要做局部的刷新,local fefrech 做全局刷新,不做局部翻新,react 跟用户进行交互,对用户的操作有所响应,用户体验好,还会用 ajax 请求,服务端和客户端之间传json页面,系统的性能会提高,这是从 ajax 角度考虑,但是它仍然是请求响应的模式。
Websocket,客户端就是浏览器和服务器不是请求相应,是服务器想主动给客户端推,股票交易市场软件,希望所有的客户端不断的收到消息,财经类网站都是一样的,不断的收到服务器端推过来的消息,在浏览器里都要去实现这样的功能,所以用 websocket,可以看到在一个系统里,四种通信方式可能都存在,但是前两种发生在后端,后两种是发生在前端和后端之间。
所以在一个系统,各种各样的通讯方式都有,像数据库,在一个系统里面不应该只用一种数据库,应该根据不同的应用,不同的场景,选择不同的数据库利用起来,同步通信,异步通信,同步通信和异步通信发生在后台,如果前端和后端发生通信,异步通信是什么样的,如果做一个前后端的双通信又是什么样的,所以这是不同的通信模式,在系统里应用的时候,四种都不一样,websocket 本身很简单,但是要把相关凑到一起理解之间应该怎么样根据不同的场景做选择,不同的场景不要认为要用一种通信模式,系统不要开发过于简单,都用一种方式。
(5)Using Encoders and Decoders
The Java API for WebSocket provides support for converting between WebSocket messages and custom Java types using encoders and decoders.
An encoder takes a Java object and produces a representation that can be transmitted as a WebSocket message;
for example, encoders typically produce JSON, XML, or binary
representations.
A decoder performs the reverse function: it reads a WebSocket message and creates a Java object.
This mechanism simplifies WebSocket applications, because it decouples the business logic from the serialization and deserialization of objects.
聊天室,写名字就会有相应的加入,把收到的消息推给所有的客户端,跑的时候开两个不同的浏览器跑。
Encoder,decoder 在发送消息或者接收消息的时候,消息的格式是自定义的,可以如何进行处理。
Implement one of the following interfaces:
Encoder.Text<T> for text messages
Encoder. Binary<T> for binary messages
public class MessageATextEncoder implements Encoder. Text<MessageA>
{
针对消息进行处理
@Override
public void init(EndpointConfig ec)
{}
@Override
public void destroy()
{}
@Override
public String encode (MessageA msgA) throws
EncodeException
{
/
/ Access msgA's properties and convert to JSON text. . .
return msgAJsonString;
}
}
decoders
Implement one of the following interfaces:
Decoder. Text<T> for text messages
Decoder. Binary<T> for binary messages
public class MessageTextDecoder implements Decoder. Text<Message> {
@Override
public void init(EndpointConfig ec){}
@Override
public void destroy(){}
@Override
public Message decode(String string) throws DecodeException {
只要有消息过来
,
给一个字符串组装成message
//Read message...
if ( /* message is an A message */ ) return new MessageA(...);
else if ( /* message is a B message */ ) return new MessageB(. ..);
@Override
public boolean willDecode(String string) {
// Determine if the message can be converted into either a
// MessageA object or a MessageB object.. .
return canDecode;
}
}
加入之后,谁说一句话,另外一方能收到。页面的第一块是名字,输入,第二块是所有的聊天记录,用户列表,底下是可选的展示一下双方通信对象。
(6)An example - Chatroom
index.html
<html xmlns="http: / /www. w3. org/1999/xhtml">
<head>
<title>WebsocketBot</title>
<script type="text/javascript">
var wsocket;
/
/ Websocket connection
var userName ;
// User's name
var textarea;
/
/ Chat area
var wsconsole;
/
/ Websocket console area
var userlist;
//
User list area
function connect()
{
textarea = document . getElementById( "textarea" );
wsconsole = document . getElementById( "wsconsole");
userlist = document . getElementById("userlist");
wsocket = new WebSocket (
"Ws://localhost : 8080/WebSocketChatRoom/ websocketbot") ;
进行连接
wsocket . onmessage = onMessage ;
回调函数
,
一旦回调成功
document . getElementById( "name"). focus();
页面刷新出来
,
光标停在输入框上
document . getElementById( " consolediv"). style.visibility =' hidden' ;
不显示
}
index.html
function onMessage(evt) {
var line = "";
var msg = JSON. parse(evt.data);
if (msg.type === "chat") {
是聊天消息
line = msg. name +’’:’’;
if (msg. target.length > 0)
line ="@" + msg.target + "" ;
line = ‘’[--’’+msg.message +" " ;
textarea. value +=’’’’+ line;
} else if (msg.type === "info") {
line = "[--" + msg.info + "--]\n";
加入的话术
,
离开的话术
textarea.
V
alue+ =+ ‘’’’line;
} else if (msg.type == "users") {
用户列表消息
line = "Users:\n";
for (var i=0; i < msg.userlist.length; i++)
line +="-" + msg.userlist[i] + "\n";
userlist.value = line;
}
textarea. scrollTop = 999999 ;
wsconsole.value +=" - >"+evt.data + "\n";
wsconsole. scrollTop = 999999;
}
三种消息各不相同,如果是聊天消息,把新的消息组装之后 line 到当前的内容中,按照内容不断增加,如果是info直接就直接在会话区显示一下,消息是什么,如果是消息列表,就刷新消息列表。
index.html
function sendJoin() {
var input = document . getElementById("input");
var name = document . getElementById("name");
V
ar join = document. getElementById("join");
var jsonstr;
if (name . value.length > 0) {
var joinMsg ={};
joinMsg. type = "join";
joinMsg. name = name. value ;
jsonstr = JSON. stringify(joinMsg);
wsocket . send(jsonstr);
name.disabled = true;
join.disabled = true;
input.disabled = false;
userName = name. value;
wsconsole. value +="<-" + jsonstr + "\n";
wsconsole. scrollTop = 999999;
}
}
在浏览器里面,如果输入谁,就把它组装成 json 对象,客户端发了消息给服务器端。
index.html
function sendMessage(evt) {
var input = document . getElementById("input");
var jsonstr;
var msgstr;
if (evt.keyCode === 13 && input. value.length > 0) {
var chatMsg = {};
chatMsg.type = "chat";
chatMsg. name = userName;
msgstr = input. value;
chatMsg.target = getTarget (msgstr.replace(/,/g, ""));
chatMsg . message = cleanTarget (msgstr);
chatMsg . message =
chatMsg . message . replace(/(lrlnllnlr)/gm,"");
jsonstr = JSON. stringify( chatMsg);
wsocket . send(jsonstr);
input.value = "";
wsconsole. value +="<- " + jsonstr + "\n";
wsconsole. scrollTop = 999999;
}
}
发送聊天消息出去,判断是不是按过回车或者是输入的密码是大于零,不是空消息,组消息,消息的类型是聊天消息,组装起来弄成 json 对象。
index.html
function checkJoin(evt) {
var name = document . getElementById("name");
var input = document . getElementById( " input" ) ;
if (evt.keyCode === 13 && name . value.length > 0) {
sendJoin();
input. focus();
}
}
function getTarget(str) {
var arr = str.split(" ");
var target = "";
for (var i=0; i<arr.length; i++) {
if (arr[i]. charAt(0) === '@') {
target = arr[i]. substring(1,arr[i] . length);
target = target. replace(/(\r\n|n\r)/gm,"");
}
}
return target ;
}
一个人加入之后按下回车健,并且还写一个名字大于0,如果是就加入消息,把输入框给过去。
index.html
function cleanTarget(str) {
var arr = str.split(" ");
var cleanstr ="";
for (var i=0; i<arr.length; i++) {
if (arr[i].charAt(0) !== '@')
cleanstr += arr[i] +'
}
return cleanstr. substring(0, cleanstr.length-1);
}
function showHideConsole() {
var chkbox = document . getElementById(" showhideconsole");
var consolediv = document . getElementById("consolediv");
if (chkbox. checked )
consolediv. style. visibility = 'visible';
else
consolediv. style. visibility = 'hidden';
}
window . addEventListener( "load", connect, false);
</script>
</head>
index.html
<body>
<h1>WebsocketBot</h1>
Your name: <input id="name" type="text" size="20" maxlength="20"
onkeyup=" checkJoin(event);"/>
<input type="súbmit" id="join" value="Join!"
onclick="sendJoin();"/><br/>
<textarea id="input" cols="70" rows="1" disabled="true"
onkeyup= " sendMessage( event); "></textarea><br/>
<textarea id="textarea" cols="70" rows="20"
readonly= "true"></textarea>
显示所有聊天消息
<textarea id="userlist" cols="20" rows="20"
用户的列表
readonly= "true">< /textarea>
<br/><br/><br/>
<input id=" showhideconsole" type= "checkbox"
onclick= " showHideConsole();"/>
Show WebSocket console<br/>
<div id=" consolediv"><textarea id= "wsconsole" cols="80" rows="8"
readonly= "true" style= "font-size: 8pt;"></textarea>/div>
</body>
</html>
页面构成里面输入名字的地方,按下键弹起时检查,用户按下设置的回车键,并且名字是否大于零,如果大于零发送有人加入的消息数据,也可以不按回车,点按钮也可以,一旦有输入消息,同样会判断是否有按键弹起,发消息,检验是否大于零。
BotEndPoint.java
@ServerEndpoint(
value = " /websocketbot" ,
decoders =
{
MessageDecoder.class
}
encoders =
{
JoinMessageEncoder . class, ChatMessageEncoder . class,
InfoMessageEncoder. class, UsersMessageEncoder. class
}
)
public class BotEndpoint
{
功能不复杂
private static final Logger logger = Logger. getLogger("BotEndpoint");
@OnOpen
public void openConnection(Session session)
{
logger . log(Level. INFO, "Connection opened.");
}
有组装和解析消息的,打开连接但是没做什么。
BotEndPoint.java
@OnMessage
public void message(final Session session, Message msg) {
if (msg instanceof JoinMessage) {
JoinMessage jmsg = (JoinMessage), msg;
sess ion. getUserProperties() . put("name", jmsg. getName() );
session . getUserProperties() . put("active", true);
logger . log(Level . INFO, "Received: [0]", jmsg. toString());
sendAll(session, new InfoMessage(jmsg . getName() + " has
joined the chat"));
sendAll(session, new ChatMessage( "Duke", jmsg. getName( ),
"Hi there!!"));
sendAll(session, new UsersMessage( this . getUserList(session)));
} else if (msg instanceof ChatMessage) {
final ChatMessage cmsg = (ChatMessage) msg;
logger . log(Level . INFO, "Received: [0]", cmsg. toString());
sendAll(session, cmsg);
}
}
判断一个消息是什么,判断谁加入,解析内容是谁,加入还是离开,把消息发送给所有的人,如果谁加入就sendal,如果是聊天消息也是 sendall。
BotEndPoint.java
public synchronized void sendAll(Session session, Object msg) {
try {
for(Session s : session. getOpenSessions()) {
if (s. isOpen()) {
s. getBasicRemote() . sendObject (msg);
logger . log(Level.INFO, "Sent: [0]", msg. toString());
}
}
] catch (IOExceptionl EncodeException e) {
logger . log(Level . INFO, e. toString());
public List<String> getUserList(Session session) {
List<String> users = new ArrayList<>( );
for (Session S : session. getOpenSessions()) {
if (s. is0pen( )&&(boolean) s. getUserProperties().get("active"))
users. add(s. getUserProperties(). get("name") . toString());
}
return users;
}
}
保持当前所有 session 的连接需求,只要开放就把消息推给它,就可以看到所有的消息是如何得到的。
Message.java
public class Message {}
ChatMessage.java
public class ChatMessage extends Message {
private String name;
private String target;
private String message;
public ChatMessage(String name, String target, String message) {
this. name = name ;
this.target = target;
this .message = message ;
}
...
public String getMessage() { return message;}
public void setMessage(String message) f this .message = message; }
}
Message 有不同的属性,usersmessage 属性不一样而已。
ChatMessageEncoder.java
public class ChatMessageEncoder, implements Encoder. Text<ChatMessage>
{
@Override
public void init( EndpointConfig ec)
{}
@Override
public void destroy()
{}
@Override
public String encode ( ChatMessage chatMessage ) throws EncodeException
StringWriter swriter = new StringWriter();
try (JsonGenerator jsonGen = Json. createGenerator(swriter))
{
jsonGen. writeStartObject( )
. write("type", "chat'
, write("name", chatMessage . getName())
. write("target" , chatMessage . getTarget( ) )
. write( "message",
chatMessage . getMessage())
. writeEnd( );
}
return swriter . toString();
}
}
针对聊天的 message,encode 里面主要是 encode 的方法,要把这个消息的内容组成一个 json 的对象,最后写出去,解析 chat 里面的内容。
MessageDecoder.java
public class MessageDecoder implements Decoder. Text <Message> {
private Map<String,String> messageMap;
@Override
public void init(EndpointConfig ec) {}
@Override
public void destroy() {}
/* Create a new Message object if the message can be decoded */
@Override
public Message decode(String string) throws DecodeException {
Message msg = null;
if (wíllDecode(string)) {
switch (messageMap. get("type")) {
case "join":
msg = new JoinMessage (messageMap. get("name"));
break;
case "chat":
msg = new ChatMessage (messageMap. get("name"),
messageMap . get( "target" ),
messageMap . get( "message"));
} else {
throw new DecodeException(string, " [Message] Can't decode.");
return msg;
}
给了字符串,如何把字符串内容组成想要的消息,一个是把消息组成 json,一个是把接收到的 json组成想要的消息,自定义怎么样的消息合适,这就是 websocket 主要的逻辑。