点对点式套接字编程
一、何为套接字
- 套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。——百度百科·套接字
- 这里的套接字主要指的是 WebSocket。
1、WebSocket
- WebSocket 为浏览器和服务端提供了双工异步通信的功能,即浏览器可以向服务器发送消息,服务端也可以向浏览器发送消息。
- Spring Boot 对 WebSocket 提供了支持。
- 由于直接使用 WebSocket 来开发程序步骤繁琐,因此通常使用它的子协议 STOMP 来开发;STOMP 是一个更高级别的协议,基于帧(frame)的格式来定义消息。
- WebSocket 有两种信息传输模式,一种是广播模式,此种模式下只要成功连接服务器且正常运行的客户端浏览器都能够收到消息;另一种是点对点模式,只有目的端浏览器才能收到消息。
二、WebSocket 简单项目
2、点对点式
- 点对点式的 web Socket 通常用于聊天室等具有数据传输目的地的程序,其形式就是一个用户向另一个用户发送数据,并且在发送数据之前还需要验证用户的身份是否合法,Spring Security 提供了这一操作的支持。
2.1、Spring Security 支持
- 在引入 Spring WebSocket 的基础上,还需要增加 Spring Security 的支持,在 POM 文件中增加如下代码:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
2.2、deploy security configuration
- 因为用到了 Spring Security,因此需要针对性地配置,避免因为没有配置导致任何访问都被拦截。
- 配置的目标可以归结为:哪些 URL 的访问无需拦截,哪些资源允许用户使用以及哪些用户是合法的。
- 要达到以上目标,可以通过继承 WebSecurityConfigurerAdapter 类并重写需要的方法来实现,代码如下:
import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/","/login", "/ws").permitAll() //根路径和/login、/ws不拦截 .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") //登陆页面 .defaultSuccessUrl("/chat") //登录成功转向该页面 .permitAll() .and() .logout() .permitAll(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("pyc").password("pyc").roles("USER") .and() .withUser("ycy").password("ycy").roles("USER"); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("src/main/resources/static/**"); //静态资源放行 } }
2.3、配置 WebSocket 增加协议节点
- 对上一篇博文中创建的 WebSocketConfig 类文件进行修改,增加协议节点:
import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/endpointWisely").withSockJS(); //注册协议节点,指定用 Sock JS registry.addEndpoint("/endpointChat").withSockJS(); //增加协议节点 } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/queue", "/topic"); // 消息代理 } }
2.4、登录页面和聊天室页面
- 聊天室页面用到了上个项目中的 JS 脚本,需要重现本程序的可以找我要。
- 首先是一个登陆页面,代码如下:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> <meta charset="UTF-8" /> <head> <title>登陆页面</title> </head> <body> <div th:if="${param.error}"> 无效的账号和密码 </div> <div th:if="${param.logout}"> 你已注销 </div> <form th:action="@{/login}" method="post"> <div><label> 账号 : <input type="text" name="username"/> </label></div> <div><label> 密码: <input type="password" name="password"/> </label></div> <div><input type="submit" value="登陆"/></div> </form> </body> </html>
- 然后是聊天室页面,代码如下:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <meta charset="UTF-8" /> <head> <title>Home</title> <script th:src="@{sockjs.min.js}"></script> <script th:src="@{stomp.min.js}"></script> <script th:src="@{jquery.js}"></script> </head> <body> <p> 聊天室 </p> <form id="ycyForm"> <label> <textarea rows="4" cols="60" name="text"></textarea> </label> <input type="submit"/> </form> <input type="button" value="离开" onclick="exit()"/> <script th:inline="javascript"> // 发送消息 $('#ycyForm').submit(function(e){ e.preventDefault(); var text = $('#ycyForm').find('textarea[name="text"]').val(); sendSpittle(text); }); //建立套接字 var sock = new SockJS("/endpointChat"); //1 var stomp = Stomp.over(sock); stomp.connect('guest', 'guest', function(frame) { stomp.subscribe("/user/queue/notifications", handleNotification);//2 }); //将接收到的消息显示出来 function handleNotification(message) { $('#output').append("<b>Received: " + message.body + "</b><br/>") } //发送消息 function sendSpittle(text) { stomp.send("/chat", {}, text);//3 } //退出聊天室 function exit(){ sock.close(); window.location.href="/login"; } $('#stop').click(function() {sock.close()}); </script> <div id="output"></div> </body> </html>
- 上面两个文件分别命名为 login.html 和 chat.html。
2.5、消息收发控制
- 消息发个谁需要一个 controller 进行控制,出于充分利用文件的原因,直接在上一篇建立的 wsController 文件进行修改:
import com.wisely.ch5_2_3.domain.WiselyMessage; import com.wisely.ch5_2_3.domain.WiselyResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Controller; import org.springframework.messaging.simp.SimpMessagingTemplate; import java.security.Principal; @Controller public class WsController { @MessageMapping("/welcome") @SendTo("/topic/getResponse") public WiselyResponse say(@org.jetbrains.annotations.NotNull WiselyMessage message) throws Exception{ Thread.sleep(300); return new WiselyResponse("Welcome,"+message.getName()+"!"); } // 以下为点对点式套接字的 Controller @Autowired private SimpMessagingTemplate messagingTemplate; @MessageMapping("/chat") public void handleChat(Principal principal,String msg){ if(principal.getName().equals("pyc")){ messagingTemplate.convertAndSendToUser("ycy", "/queue/notifications",principal.getName()+"-send:"+msg); }else{ messagingTemplate.convertAndSendToUser("pyc", "/queue/notifications",principal.getName()+"-send:"+msg); } } }
2.6、MVC Configuration
- 对刚刚新建的两个 view 进行 URL 设置,代码如下:
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @Configuration public class WebMvcConfig extends WebMvcConfigurerAdapter { @Override public void addViewControllers(ViewControllerRegistry registry){ registry.addViewController("/ws").setViewName("/ws"); registry.addViewController("/login").setViewName("/login"); registry.addViewController("/chat").setViewName("/chat"); } }
2.7、运行测试
- 运行项目,分别用两个浏览器访问 https://localhost:8080/login,浏览器渲染如下:
- 在两个浏览器的账号输入框分别输入 pyc 和 ycy,密码就是账号名,点击登录,页面跳转到聊天室页面:
- 在文本域输入信息
- 点击提交,查看另外一个浏览器
- 可以看到收到了消息,在这个浏览器也发送信息,同理另外一个浏览器也会收到消息