注意:socket只是实现一些简单的功能,具体的还需根据自身情况,代码稍微改造下
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.2.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.cyb</groupId> <artifactId>socket_test</artifactId> <version>0.0.1-SNAPSHOT</version> <name>socket_test</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!-- springboot websocket --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <!--guava依赖--> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version> </dependency> <!--fastjson依赖--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.46</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>
package com.cyb.socket.websocket;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.socket.server.standard.ServerEndpointExporter;@Configurationpublic class WebSocketStompConfig { //这个bean的注册,用于扫描带有@ServerEndpoint的注解成为websocket ,如果你使用外置的tomcat就不需要该配置文件 @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); }}
package com.cyb.socket.websocket;import java.io.IOException;import java.util.Map;import java.util.Set;import java.util.concurrent.ConcurrentHashMap;import javax.websocket.OnClose;import javax.websocket.OnError;import javax.websocket.OnMessage;import javax.websocket.OnOpen;import javax.websocket.Session;import javax.websocket.server.PathParam;import javax.websocket.server.ServerEndpoint;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import com.google.common.collect.Maps;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;/** * @Author:陈彦斌 * @Description:Socket核心类 * @Date: 2020-07-26 */@Component@ServerEndpoint(value="/connectWebSocket/{userId}")public class WebSocket { private Logger logger=LoggerFactory.getLogger(this.getClass()); /** * 在线人数 */ public static int onlineNumber=0; /** * 以用户的姓名为key,WebSocket为对象保存起来 */ private static Map<String, WebSocket> clients=new ConcurrentHashMap<String, WebSocket>(); /** * 会话 */ private Session session; /** * 用户名称 */ private String userId; /** * 建立连接 * * @param session */ @OnOpen public void onOpen(@PathParam("userId") String userId, Session session) { onlineNumber++; System.out.println("现在来连接的客户id:" + session.getId() + "用户名:" + userId); //logger.info("现在来连接的客户id:"+session.getId()+"用户名:"+userId); this.userId=userId; this.session=session; System.out.println("有新连接加入! 当前在线人数" + onlineNumber); // logger.info("有新连接加入! 当前在线人数" + onlineNumber); try { //messageType 1代表上线 2代表下线 3代表在线名单 4代表普通消息 //先给所有人发送通知,说我上线了 Map<String, Object> map1=Maps.newHashMap(); map1.put("messageType", 1); map1.put("userId", userId); sendMessageAll(JSON.toJSONString(map1), userId); //把自己的信息加入到map当中去 clients.put(userId, this); System.out.println("有连接关闭! 当前在线人数" + onlineNumber); //logger.info("有连接关闭! 当前在线人数" + clients.size()); //给自己发一条消息:告诉自己现在都有谁在线 Map<String, Object> map2=Maps.newHashMap(); map2.put("messageType", 3); //移除掉自己 Set<String> set=clients.keySet(); map2.put("onlineUsers", set); sendMessageTo(JSON.toJSONString(map2), userId); } catch (IOException e) { System.out.println(userId + "上线的时候通知所有人发生了错误"); //logger.info(userId+"上线的时候通知所有人发生了错误"); } } @OnError public void onError(Session session, Throwable error) { //logger.info("服务端发生了错误"+error.getMessage()); //error.printStackTrace(); System.out.println("服务端发生了错误:" + error.getMessage()); } /** * 连接关闭 */ @OnClose public void onClose() { onlineNumber--; //webSockets.remove(this); clients.remove(userId); try { //messageType 1代表上线 2代表下线 3代表在线名单 4代表普通消息 Map<String, Object> map1=Maps.newHashMap(); map1.put("messageType", 2); map1.put("onlineUsers", clients.keySet()); map1.put("userId", userId); sendMessageAll(JSON.toJSONString(map1), userId); } catch (IOException e) { System.out.println(userId + "下线的时候通知所有人发生了错误"); //logger.info(userId+"下线的时候通知所有人发生了错误"); } //logger.info("有连接关闭! 当前在线人数" + onlineNumber); //logger.info("有连接关闭! 当前在线人数" + clients.size()); System.out.println("有连接关闭! 当前在线人数" + onlineNumber); } /** * 收到客户端的消息 * * @param message 消息 * @param session 会话 */ @OnMessage public void onMessage(String message, Session session) { try { //logger.info("来自客户端消息:" + message+"客户端的id是:"+session.getId()); System.out.println("来自客户端消息:" + message + " | 客户端的id是:" + session.getId()); JSONObject jsonObject=JSON.parseObject(message); String textMessage=jsonObject.getString("message"); String fromuserId=jsonObject.getString("userId"); String touserId=jsonObject.getString("to"); //如果不是发给所有,那么就发给某一个人 //messageType 1代表上线 2代表下线 3代表在线名单 4代表普通消息 Map<String, Object> map1=Maps.newHashMap(); map1.put("messageType", 4); map1.put("textMessage", textMessage); map1.put("fromuserId", fromuserId); if (touserId.equals("All")) { map1.put("touserId", "所有人"); sendMessageAll(JSON.toJSONString(map1), fromuserId); } else { map1.put("touserId", touserId); System.out.println("开始推送消息给" + touserId); sendMessageTo(JSON.toJSONString(map1), touserId); } } catch (Exception e) { e.printStackTrace(); //logger.info("发生了错误了"); } } /** * 给指定的用户发送消息 * * @param message * @param TouserId * @throws IOException */ public void sendMessageTo(String message, String TouserId) throws IOException { for (WebSocket item : clients.values()) { System.out.println("给指定的在线用户发送消息,在线人员名单:【" + item.userId.toString() + "】发送消息:" + message); if (item.userId.equals(TouserId)) { item.session.getAsyncRemote().sendText(message); break; } } } /** * 给所有用户发送消息 * * @param message 数据 * @param FromuserId * @throws IOException */ public void sendMessageAll(String message, String FromuserId) throws IOException { for (WebSocket item : clients.values()) { System.out.println("给所有在线用户发送给消息,在线人员名单:【" + item.userId.toString() + "】发送消息:" + message); item.session.getAsyncRemote().sendText(message); } } /** * 给所有在线用户发送消息 * * @param message 数据 * @throws IOException */ public void sendMessageAll(String message) throws IOException { for (WebSocket item : clients.values()) { System.out.println("服务器给所有在线用户发送消息,当前在线人员为【" + item.userId.toString() + "】发送消息:" + message); item.session.getAsyncRemote().sendText(message); } } /** * 获取在线用户数 * * @return */ public static synchronized int getOnlineCount() { return onlineNumber; }}
package com.cyb.socket.websocket;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.*;import java.io.IOException;@Controller@RequestMapping("testMethod")public class TestController { @Autowired private WebSocket webSocket; /** * 给指定的在线用户发送消息 * @param userId * @param msg * @return * @throws IOException */ @ResponseBody @GetMapping("/sendTo") public String sendTo(@RequestParam("userId") String userId,@RequestParam("msg") String msg) throws IOException { webSocket.sendMessageTo(msg,userId); return "推送成功"; } /** * 给所有在线用户发送消息 * @param msg * @return * @throws IOException * @throws IOException */ @ResponseBody @PostMapping("/sendAll") public String sendAll(@RequestBody String msg) throws IOException, IOException { webSocket.sendMessageAll(msg); return "推送成功"; }}
package com.cyb.socket.schedule;import com.cyb.socket.websocket.WebSocket;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Component;import java.io.IOException;import java.text.SimpleDateFormat;import java.util.Date;@Componentpublic class SocketTask { @Autowired private WebSocket webSocket; private SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS" ); //5秒轮询一次 @Scheduled(fixedRate=5000) public void sendClientData() throws IOException { String msg="{\"message\":\"你好\",\"userId\":\"002\",\"to\":\"All\"}"; webSocket.sendMessageAll(msg); System.out.println("消息推送时间:"+ sdf.format(new Date())); }}
注意需要加上这些注解
演示
前做web应用一直是在本地装个Apache、Tomcat之类的软件,然后把做好的网页文件放在他们的工作目录下(如Apache的htdocs),然后打开浏览器输入127.0.0.1或localhost就可以直接访问了,好神奇,可是为什么,怎么实现的呢,早就知道有Socket(套接字)这个东西,可之前就是没有把这两方面结合起来,今天我们就一起来看一看这究竟是为什么。
有同学说还不懂Socket是什么,这东西很抽象,在计算机网络原理里讲协议时才会看到,今天咱们完全忽略太严谨、学术的定义,就来看看Socket到底是什么。想象一下,你把电脑的电源插在插座上,你的电脑就可以使用了,为什么?“这不是废话吗!”确实,咱们来想一下这个过程,你拿着插头插在了插座上,然后你的电脑和千里之外的供电厂就能“通信”了,把你的电脑想成是客户端,把千里之外的供电厂想成是服务器,通过插座和很长很长的线缆你们就可以勾搭上了,那么Socket在这其中相当于什么呢?“插座!”没错,就是插座!对于我的电脑来说,我想让它通电工作,我只需要个插座就行了啊,什么插座是什么材质的,线缆是什么型号的,供电厂到底在什么经纬度,电力到底怎么传输,我管它干嘛呢,都跟我没关系!我只要知道我需要的不是整个世界,而是。。。一个插座!读到这里,想必同学已经对“插座”有了很森的理解了;再举一个例子,你和基友的电脑通过有线的方式连上了同一个路由器,这个时候你们就可以直接通过内网IP地址进行访问了,在这个过程中,那个方方的接口(RJ45接口)就是“Socket”,反正插上“Socket”就能用,我不用管到底通过Socket怎么能够实现通信。在计算机编程的网络世界里,作为应用程序,我只需要一个“插座”就可以和任何服务器通信了,想想都有点小激动呢~~~
接下来要讲的就是,电脑电源需要一个socket去插上,那么发电厂呢,也同样需要一个插座插上去来给你供电——也就是说,发电厂需要一个“插座”!。。。废话,,,,没错,确实是这样,服务器端也需要一个“插座”,只不过它叫做ServerSocket(这看起来像是继承自Socket,我也不知道,待查)。
有了“插座”(Socket)的概念之后,我们就可以愉快地让电脑(客户端)与发电厂(服务器)通信了。无论是客户端还是服务器,都需要Socket,鉴于咱们今天的题目是“搭建web服务器”,所以咱们接下来就来看一下怎么创建服务器的ServerSocket。说道这里,有同学就会问到了,“难道客户端不需要Socket吗?”,确实需要,因为我们是用浏览器访问本机IP“127.0.0.1”,所以客户端的Socket就由浏览器自己维护了,不需要咱们动手写的。“可是我还是不明白为什么在浏览器里输入127.0.0.1之后就可以看到我的网页了?求解释” 好,那咱们慢慢来,先动手编写一个服务器端的ServerSocket吧啦啦啦~
创建服务器端Socket的步骤如下:
1、创建ServerSocket对象
ServerSocket serverSocket=new ServerSocket(“80”); //这里只需要指明当前程序监听80号端口就可以了,至于为什么是80,因为我喜欢!“好霸道。。。”因为我们要监听web请求,默认就是80号端口。其实,1-1024端口被操作系统占用了,1025-65535的端口你随便用,只要不会和其他应用程序冲突就可以(别用什么类似3389这么常用的端口就好了。。。)
2、作为服务器,我要知道,我的使命就是要等待客户端发来请求,也就是客户端发来Socket,我首先要把它Hold住!
Socket socket=serverSocket.accept(); //这里需要特别说明一下,accept方法比较特殊,它是一个阻塞方法(block method),因为只要它等不来客户端发来的请求(Socket),它就一直等下去而不会继续执行它下面的代码。唉,此等痴情人怎么跟我一样O(∩_∩)O
3、客户端要向我表白,给我发来情书,那我作为服务器只要得到它的输入就好了
InputStream inputStream=socket.getInputStream(); //注意,客户端发来的表白信息都在socket里面,而不是serverSocket里面,这点要是弄错了,读不到情书内容,活该你单身。(我只有冷笑。。。)
4、收到了情书,我好想知道里面究竟写了什么啊!迫!不!及!待! 好,开始解析情书内容
BufferedReader reader=new BufferedReader(new InputStreamReader(inputStream)); //java包装类,只为读到写给我的情书,耶~ String line=“”; while ( (line=reader.readLine()) !=null ){ System.out.println(line); }
5、组装前4步的代码,会要求try catch一下异常,正常捕获就好 下面贴代码
public class MultiWebServer { public static void main(String[] args) { try { ServerSocket serverSocket=new ServerSocket(80); System.out.println("正在等待情书中..."); Socket socket=serverSocket.accept(); System.out.println("收到情书,我要开始解析!"); InputStream inputStream=socket.getInputStream(); BufferedReader reader=new BufferedReader(new InputStreamReader(inputStream)); String line=""; while ( (line=reader.readLine()) !=null ){ System.out.println(line); } } catch (Exception e) { e.printStackTrace(); } } }
好了,服务器端的代码咱们写完了,那接下来干啥?不知道。。。不过还记得刚才提出的问题吗——“可是我还是不明白为什么在浏览器里输入127.0.0.1之后就可以看到我的网页了?”那就试试呗,看看咱们如果在浏览器里输入127.0.0.1或者localhost会怎么样
首先必须把刚才咱们编写的服务器端程序运行起来,然后再打开浏览器,记住,必须先运行服务器端程序,不然情书就发丢了。。。运行服务器端程序,如图:
注意红圈中的两点:由于此时没有客户端发来情书,还记得刚才的accept()阻塞方法吗,它就一直等啊等,等不来我还等,所以红圈中会显示“正在等待情书中…”;那么右面那个箭头指向的是什么意思呢,一个红色停止的图标,也就是说,这个程序现在一直在执行着,没有结束,就好像死循环一样(当然这里绝对不是死循环,其实是阻塞,只是死的样子好像死循环,一会咱们会谈到死循环的,别着急,迟早会死的)
接下来打开浏览器,在地址栏输入127.0.0.1/index.html后回车,看看浏览器什么反应。。。。。一段时间过去了,浏览器居然一点反应都没有,然后告诉我该页无法显示。我去。。。难道讲了这么多咱们就这么失败了吗,我哭。那就打开eclipse看一眼吧,看看服务器端有没有什么动静啊。打开服务器端一看,卧槽,瞬间世界向我问好了!
注意看红笔标注,我收到了情书!我要开始解析了!那到底情书里是什么内容,别问我,继续向下看。“好熟悉的一段报文,我们好像在哪见过,还记得吗,那是一个春天,你刚发芽儿。。。”没错,这就是计算机网络原理讲的http请求报文。没有学过计网怎么办,没关系,看前两行(其实我们一会用到的也只有第一行而已),“我看到了index.html” 是的,这是刚才我们在浏览器里面输入的地址;第二行,“我也看到了127.0.0.1”,是的,也是我们刚刚在浏览器里面输入的。这说明了什么?激动的我无法说出这到底说明了什么,但想必读者你已经揣测出了什么。
写到这里,作为服务器的我已经收到了从客户端发来的情书,那客户端(浏览器)为什么一点反应都没有呢,甚至过了一会就“该页无法显示”了。因为啊,人家给你写了情书,你没回复人家,人家等了一会觉得没戏了就伤心欲绝了!是啊,喜不喜欢人家都要和他说一声的,给他个答复,哪怕只说:“对不起,你是个好人。。。”
走神了吧?好像说到自己了吧?回来吧,咱们现在的任务呢,就是怎么给人家个答复。
怎么给,怎么给,怎么给。。。快想快想,既然人家都指明了想和127.0.0.1里的“index.html”表白,那当然就得由index.html来给他答复喽。怎么答,怎么答,怎么答。。。快想快想,既然index.html是个文件,那我读出文件内容后直接发给客户端不就行了吗?可是用什么发?没错,是socket!我们用socket把文件的内容返回给客户端就好了。
那么问题来了。。。“说的非常好,关键是怎么做!”——首先怎么读出文件来?
假定咱们的index.html在我电脑的E://课件/计算机网络原理/实验/实验1/ 文件夹下,并且假定不会跨域访问,则:
1、定义一个字符串,用来存咱们的工作目录
String base_url="E://课件/计算机网络原理/实验/实验1/"; //这只是我本机的目录,至于到了你的电脑上,你可以自己更改</span>
2、我怎么通过报文知道客户端要和index.html表白?看情书第一行 GET /index.html HTTP/1.1,所以只需要获取情书的第一行字符串并解析出index.html就好办啦,easy,开始吧
//由于目前只需要第一行,所以咱们就不像上面那样循环读取了,读一行就够了 String line=reader.readLine(); //用字符串截取函数,把“index.html”这个字符串给揪出来 String url=line.substring(5, line.indexOf("HTTP") - 1);
3、所以咱们index.html的绝对路径就是 base_url + url 了,终于把我爱的人从人山人海中找到了,看看她怎么答复我吧——获取文件内容
inputStream=new FileInputStream(base_url + url); OutputStream outputStream=socket.getOutputStream(); //我要从服务器给客户端答复了,对于服务器来说,这是发出去的内容,所以是Out! byte[] buffer=new byte[4 * 1024]; //定义字节缓冲区 int len=0; while ((len=inputStream.read(buffer)) !=-1) { outputStream.write(buffer, 0, len); //很重要!通过socket的outputStream把咱们解析出来的文件内容一字不落的发出去 如果没写这个,导致你爱的跟你表白的抑郁而死,活该你单身 } outputStream.flush(); //如果最后一次write时没有把buffer写满,是不会自动发出去的,需要调用flush方法强制把内容从缓冲区发出去
好了,文件读取出来了,也返回给客户端了,亲爱哒他能收到吗?还是一样,务必先运行服务器端程序,然后打开浏览器输入127.0.0.1/index.html 后回车。我紧张,我激动,能不能收到回复,会给我什么样的回复?如图。。。
为什么会这样???!!! 好吧,看看女神的index.html文件里都写了些什么。。。
<html> <head> <meta charset="utf-8" /> <title>Welcome</title> </head> <body> <h1>王欢,你是个好人... </h1> </body> </html>
看到这里,我到底应该高兴还是欲绝。。。高兴的是,我女神给我答复了;欲绝的是。。。那么问题来了,,,学表白技术哪家强?
玩笑归玩笑,那我们的针对这次的浅谈题目是不是就完成了?可以说是的,但是我表白一次失败就算了?我还要表白第二次!(其实我倒不是这样的,这里只能牺牲我的人品来为了大家更好的理解了,呵呵)。好吧,我刚才的工作目录下还有个another.html,这次我来跟她表白吧!好!继续在浏览器中输入127.0.0.1/another.html后回车,期待这次会表白成功。可是我等啊等,浏览器在那里打圈圈,难道浏览器都知道我太花心了,拒绝帮我传递情书?好吧,我再打开浏览器试一下,输入127.0.0.1/index.html ,嗯?连第一个女神都不理我了?!我靠!为毛!
冲动是魔鬼!冷静!我打开eclipse控制台,发现服务器根本就没有“正在等待情书中…”,所以我拜托浏览器发过去的情书当然就发丢了,因为根本没人在接收啊。(窃喜,还好不是因为我太花心了所以浏览器没有帮我投递情书)可是为什么呢?
冷静吧,分析代码。其实我们可以想到,这段代码执行完一次后不就结束了吗,那我第二次给她发请求她当然会收不到了。对啊,那为了解决这个问题,怎么办呢?跪求红娘支招!
红娘说:“给服务器程序个死循环吧,让她反复在等客户端的请求就好了。”(其实红娘就一直在死循环中)
红娘果然是红娘(不然是谁。。。),那就按照她的说法试一试呗!改代码,加入 while (true) 死循环:
public class MultiWebServer { public static void main(String[] args) { String base_url="E://课件/计算机网络原理/实验/实验1/"; while (true) { try { ServerSocket serverSocket=new ServerSocket(80); System.out.println("正在等待情书中..."); Socket socket=serverSocket.accept(); System.out.println("收到情书,我要开始解析!"); InputStream inputStream=socket.getInputStream(); BufferedReader reader=new BufferedReader( new InputStreamReader(inputStream)); String line=reader.readLine(); System.out.println(line); String url=line.substring(5, line.indexOf("HTTP") - 1); System.out.println("情书解析完毕,我要想想怎么回复了..."); // 获取文件内容 inputStream=new FileInputStream(base_url + url); OutputStream outputStream=socket.getOutputStream(); byte[] buffer=new byte[4 * 1024]; int len=0; while ((len=inputStream.read(buffer)) !=-1) { outputStream.write(buffer, 0, len); } outputStream.flush(); System.out.println("情书请求已发送给客户端"); //关闭对应的资源 serverSocket.close(); socket.shutdownInput(); socket.close(); inputStream.close(); reader.close(); outputStream.close(); } catch (Exception e) { } } } }
这样,这位红娘就在这里一直等啊等,来了一个客户端我就处理他的情书请求,处理完这个继续循环以相同的方式等,处理,等,处理。。。。
好吧,咱们接下来试一下,还是务必先运行服务器端程序,然后先和第一个女神表白,即 127.0.0.1/index.html 还是好朋友的话,就别问我返回结果。。。这个时候打开eclipse的控制台,有没有发现右上角的红色暂停标志可以点击,那就说明咱们的红娘还在兢兢业业的工作着!好了,抓紧时间赶紧向第二个女神表白,看她怎么说, 浏览器输入 127.0.0.1/another.html ,回车!好快啊,女神给我答复了。。。
这。。。(她怎么知道不到十分钟?你是不是突然想到了cookie可以记录客户端的信息,不过咱们这里没用到cookie)还是看看another.html文件里写了什么吧
<html> <head> <meta charset="utf-8" /> <title>Welcome</title> </head> <body> <h1>我记得你刚和别人表白吧,还不过十分钟,你怎么会是个好人!</h1> </body> </html>
好吧,我啥也不说了,亲们,我还要向第三个女神表白吗。。。?浏览器主动跟我说:“你表白吧,这次你发多少封表白信我都能给你送到服务器那里,因为她一直在等待着我给她发送呢!”想想,还是算了,人生如此,何须多言。。。
代码都贴出来了,其实看起来挺简单的,但是实际操作中会碰到各种各样的问题。
还有一些要再继续唠叨的边角料:
A:这是一个比较抽象的概念,是为了进程间通信,每一个进程只能占用一个端口,也就是说多个进程绝不能同时占用一个端口
A:常说的web服务使用80端口指的是服务器监听web请求的端口,是服务器,不是你自己的客户机。一般来说,一个应用程序打开后访问网络本地操作系统为其分配的端口号是随机的,所以三个浏览器虽然同时接收web服务器的回复报文,由于他们三个各自占用的端口不一样,所以不会产生冲突。
A:靠Socket!通过刚才的编程实战,在我理解,Socket肯定会至少包括四部分内容:IP地址,端口号,输入流和输出流。也就是说,从客户机发给服务器的Socket里一定会有客户机的IP地址和相应应用程序的端口号,这样服务器自然就知道应该把应答报文发给谁了。
A:不一定。我们刚才在编程的时候确实使用的是80端口,所以我们在浏览器中输入127.0.0.1/index.html,浏览器会默认认为我们会向127.0.0.1主机的80号端口发送请求。但是,这个80端口号只是默认的而已,我们完全可以自己改掉,比如在java代码里把服务器端的ServerSocket改成 ServerSocket serverSocket=new ServerSocket(3456); 这时候我们在浏览器中就要输入 127.0.0.1:3456/index.html 了,效果是一样的,可以浅尝辄止一下。
A:咱们只有一台电脑,这台电脑既充当着客户端的角色,又充当着服务器的角色。当浏览器请求网页时,它是客户端;当80端口收到请求报文并应答时,它就是服务器。实在不理解,就想想什么是自恋吧,或者,自交也勉强可以。。
对于此用java编写的web服务器的一点简单说明:此段代码非常简单,所以肯定不会是真正web服务器所用到的代码,咱们这个只是能够应答最最基本的web请求,不能检测是否跨域访问等等。不过最基本的,用的socket编程是肯定的。另外,对于此段程序,只给出了处理输入输出流的一种方式。对于输入流,除了咱们刚才用到的BufferedReader包装类,还可以直接用InputStream的read()方法等;对于输出流,除了咱们刚才用的OutputStream的write()方法,还可用BufferedWritter,PrintWritter等,这些都是java IO的基本用法,根据网络环境,根据所要读取的文件大小来时时变通,这就是仁者见仁智者见智了。
文章写到了最后,不知道该怎么收尾了,安安静静做个程序员吧,挺好。
.概述
大家好我是码农小胖哥,在本文中,我们将介绍客户端 - 服务器通信的基础知识,并通过今天提供的两种流行选项进行探索。我们将看到作为新进入者的WebSocket如何与更受欢迎的RESTful HTTP选择相媲美。
2.网络通信基础知识
在深入探讨不同选项及其优缺点的细节之前,让我们快速刷新网络通信的前景。这将有助于将事情放在透视中并更好地理解这一点。
根据开放系统互联通信模型(OSI)可以最好地理解网络通信。
OSI划分为七层抽象:
在这个模型的顶部是Application层,这是我们在本教程中感兴趣的。但是,在我们比较WebSocket和RESTful HTTP时,我们将讨论前四个层中的一些方面。
应用程序层最接近最终用户,负责与参与通信的应用程序连接。在这一层中使用了几种流行的协议,如FTP,SMTP,SNMP,HTTP和WebSocket。
3.描述WebSocket和RESTful HTTP
虽然可以在任意数量的系统之间进行通信,但我们对客户端 - 服务器通信特别感兴趣。更具体地说,我们将专注于Web浏览器和Web服务器之间的通信。这是我们用来比较WebSocket和RESTful HTTP的框架。
但在我们继续前进之前,为什么不快速了解它们是什么呢!
3.1 WebSockets
正如定义所述,WebSocket是一种通信协议,它通过持久TCP连接进行双向,全双工通信。现在,我们将继续详细了解本声明的每个部分。
WebSocket 在2011年由IETF标准化为RFC 6455的通信协议。如今大多数现代Web浏览器都支持WebSocket协议。
3.2 RESTful HTTP
虽然我们都知道HTTP,因为它在互联网上无处不在,但它也是一种应用层通信协议。HTTP是一种基于请求 - 响应的协议,我们将在本教程后面再次理解这一点。
REST(Representational State Transfer)是一种体系结构样式,它在HTTP上设置一组约束来创建Web服务。
4. WebSocket子协议
虽然WebSocket定义了客户端和服务器之间双向通信的协议,但它不会对要交换的消息施加任何条件。作为子协议谈判的一部分,通信方可以同意这一点。
为非平凡的应用程序开发子协议是不方便的。幸运的是,有许多流行的子协议,如STOMP可供使用。STOMP代表简单文本导向的消息传递协议,适用于WebSocket。Spring Boot拥有对STOMP的一流支持,我们将在本教程中使用它。
5. Spring Boot中的快速设置
没有比看到一个有效的例子更好的了。因此,我们将在WebSocket和RESTful HTTP中构建简单的用例,以进一步探索它们,然后进行比较。让我们为两者创建一个简单的服务器和客户端组件。
我们将使用JavaScript创建一个简单的客户端,它将发送一个名称。而且,我们将使用Java创建一个服务器,它将以问候语进行响应。
5.1。的WebSocket
要在Spring Boot中使用WebSocket,我们需要加入webSocket的功能组件:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
我们现在将配置STOMP端点:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketMessageBrokerConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/app");
config.enableSimpleBroker("/topic");
}
}
让我们快速定义一个简单的WebSocket服务器,它接受一个名字并用问候语回复:
@Controller
public class WebSocketController {
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Greeting greeting(Message message) throws Exception {
return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");
}
}
最后,让我们构建客户端以与此WebSocket服务器进行通信。在我们强调浏览器到服务器的通信时,让我们用JavaScript创建一个客户端:
var stompClient=null;
function connect() {
stompClient=Stomp.client('ws://localhost:8080/ws');
stompClient.connect({}, function (frame) {
stompClient.subscribe('/topic/greetings', function (response) {
showGreeting(JSON.parse(response.body).content);
});
});
}
function sendName() {
stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
}
function showGreeting(message) {
$("#greetings").append("<tr><td>" + message + "</td></tr>");
}
这完成了我们WebSocket服务器和客户端的工作示例。代码存储库中有一个HTML页面,它提供了一个简单的用户界面来进行交互。
虽然这只是表面上的问题,但是使用Spring的WebSocket可以用来构建复杂的聊天客户端等等。
5.2。RESTful HTTP
我们现在将为RESTful服务进行类似的设置。我们的简单Web服务将接受带有名称的GET请求并以问候语进行响应。
我们这次使用Spring Boot的Web组件:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
现在,我们将利用Spring中提供的强大注释支持来定义REST端点:
@RestController
@RequestMapping(path="/rest")
public class RestAPIController {
@GetMapping(path="/{name}", produces="application/json")
public String getGreeting(@PathVariable("name") String name)
{
return "{\"greeting\" : \"Hello, " + name + "!\"}";
}
}
最后,让我们用JavaScript创建一个客户端:
var request=new XMLHttpRequest()
function sendName() {
request.open('GET', 'http://localhost:8080/rest/'+$("#name").val(), true)
request.onload=function () {
var data=JSON.parse(this.response)
showGreeting(data.greeting)
}
request.send()
}
function showGreeting(message) {
$("#greetings").append("<tr><td>" + message + "</td></tr>");
}
这就是它!同样,代码存储库中有一个HTML页面,用于处理用户界面。
虽然其简洁性很深,但定义生产级REST API可以完成更广泛的任务!
6. WebSocket和RESTful HTTP的比较
在创建了WebSocket和RESTful HTTP的最小但有效的示例后,我们现在已经准备好了解它们如何相互对抗。我们将在下一小节中针对几个标准对此进行检查。
值得注意的是,虽然我们可以直接比较HTTP和WebSocket,因为它们都是应用程序层协议,但将REST与WebSocket进行比较并不自然。正如我们之前看到的,REST是一种利用HTTP进行通信的架构风格。REST并不是一个规范,一个协议,只是对http请求状态的表述。
因此,我们与WebSocket的比较主要是关于HTTP中的功能或缺乏功能。
6.1 网址方案
URL 定义Web资源的唯一位置和检索它的机制。在客户端 - 服务器通信中,我们通常希望通过其关联的URL获取静态或动态资源。
我们都熟悉HTTP URL方案:
http://localhost:8080/rest
WebSocket URL方案也没有太大的不同:
ws://localhost:8080/ws
一开始,唯一的区别似乎是冒号之前的字符,但它抽象了很多,发生在引擎盖下。让我们进一步探讨。
6.2。握手
握手 是指在通信方之间协商通信协议的自动方式。HTTP是一种无状态协议,适用于请求 - 响应机制。在每个HTTP请求上,通过套接字与服务器建立TCP连接。
然后客户端等待,直到服务器响应资源或错误。来自客户端的下一个请求重复所有内容,就好像之前的请求从未发生过一样:
与HTTP相比,WebSocket的工作方式非常不同,并且在实际通信之前以握手开始。
让我们看看WebSocket握手的组成部分:
对于WebSocket,客户端在HTTP中启动协议握手请求,然后等待服务器响应接受从HTTP升级到WebSocket。
当然,由于协议握手是通过HTTP发生的,因此它遵循上图中的序列。但是一旦建立连接,就从客户端和服务器上切换到WebSocket进行进一步的通信。
6.3 连接
正如我们在上一小节中看到的,WebSocket和HTTP之间的一个明显区别是WebSocket在持久TCP连接上工作,而HTTP为每个请求创建一个新的TCP连接。
现在显然为每个请求创建新的TCP连接并不是非常高效,HTTP也没有意识到这一点。实际上,作为HTTP / 1.1的一部分,引入了持久连接以缓解HTTP的这一缺点。
尽管如此,WebSocket的设计初衷是为了使用持久的TCP连接。
6.4 通讯
WebSocket over HTTP的好处是一个特定的场景,这个场景源于客户端服务器可以用旧的HTTP无法实现的方式进行通信。
例如,在HTTP中,通常客户端发送该请求,然后服务器响应请求的数据。服务器没有通用的方式来自己与客户端通信。当然,已经设计出模式和解决方案来规避服务器发送事件(SSE),但这些并不完全自然。
使用WebSocket,处理持久性TCP通信,服务器和客户端都可以相互独立地发送数据,事实上,对于许多通信方来说!这被称为双向通信。
WebSocket通信的另一个有趣特性是它是全双工的。现在虽然这个词可能听起来很深奥; 它只是意味着服务器和客户端都可以同时发送数据。将此与HTTP中发生的情况进行比较,其中服务器必须等待它才能完全接收请求,然后才能响应数据。
虽然双向和全双工通信的好处可能不会立即显现出来。我们将看到一些用户解锁一些真正的力量的用例。
6.5。安全
最后但同样重要的是,HTTP和WebSocket都利用了TLS的优势来提高安全性。虽然HTTP提供HTTPS作为使用该网页的网址方案的一部分,已经的WebSocket WSS为同样的效果的URL方案的一部分。
因此,前一小节中的URL的安全版本应如下所示:
https://localhost:443/rest
wss://localhost:443/ws
保护RESTful服务或WebSocket通信是一个非常深入的主题,这里不能涵盖。现在,我们只是说两者在这方面得到了充分的支持。
7.使用场景有哪些?
现在,我们已经看到足够的基于HTTP的RESTful服务和基于WebSocket的简单通信,以形成我们对它们的看法。但是我们应该在哪里使用什么?
重要的是要记住,虽然WebSocket出现了HTTP中的缺点,但实际上并不是HTTP的替代品。所以他们都有自己的位置和用途。让我们快速了解我们如何做出决定。
对于服务器需要偶尔进行通信的大部分情况,例如获取员工的记录,使用REST服务而不是HTTPS仍然是明智的。但对于较新的客户端应用程序,例如需要从服务器进行实时更新的股票价格应用程序,利用WebSocket非常方便。
概括来讲,WebSocket更适用于基于推送和实时通信更恰当地定义需求的情况。此外,WebSocket适用于需要同时将消息推送到多个客户端的情况。在这些情况下,通过RESTful服务进行客户端和服务器通信会发现很难。因为http是客户端到服务器的请求-应答处理。
然而,需要从需求中提取通过HTTP使用WebSocket和RESTful服务。就像没有银弹一样,我们不能指望选择一个来解决每一个问题。因此,我们必须运用智慧和知识来设计有效的沟通模式。
代码示例在我码云仓库:https://gitee.com/felord/springboot-websocket.git
以上就上对rest和websocket的简单表述和讲解。我是码农小胖哥,请多多关注!
*请认真填写需求信息,我们会在24小时内与您取得联系。