注意: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,因為我喜歡!“好霸道。。?!币驗槲覀円O聽web請求,默認就是80號端口。其實,1-1024端口被操作系統占用了,1025-65535的端口你隨便用,只要不會和其他應用程序沖突就可以(別用什么類似3389這么常用的端口就好了。。。)
2、作為服務器,我要知道,我的使命就是要等待客戶端發來請求,也就是客戶端發來Socket,我首先要把它Hold?。?/p>
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”,是的,也是我們剛剛在瀏覽器里面輸入的。這說明了什么?激動的我無法說出這到底說明了什么,但想必讀者你已經揣測出了什么。
寫到這里,作為服務器的我已經收到了從客戶端發來的情書,那客戶端(瀏覽器)為什么一點反應都沒有呢,甚至過了一會就“該頁無法顯示”了。因為啊,人家給你寫了情書,你沒回復人家,人家等了一會覺得沒戲了就傷心欲絕了!是啊,喜不喜歡人家都要和他說一聲的,給他個答復,哪怕只說:“對不起,你是個好人。。。”
走神了吧?好像說到自己了吧?回來吧,咱們現在的任務呢,就是怎么給人家個答復。
怎么給,怎么給,怎么給。。??煜肟煜耄热蝗思叶贾该髁讼牒?27.0.0.1里的“index.html”表白,那當然就得由index.html來給他答復嘍。怎么答,怎么答,怎么答。。??煜肟煜?,既然index.html是個文件,那我讀出文件內容后直接發給客戶端不就行了嗎?可是用什么發?沒錯,是socket!我們用socket把文件的內容返回給客戶端就好了。
那么問題來了。。?!罢f的非常好,關鍵是怎么做!”——首先怎么讀出文件來?
假定咱們的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中,通??蛻舳税l送該請求,然后服務器響應請求的數據。服務器沒有通用的方式來自己與客戶端通信。當然,已經設計出模式和解決方案來規避服務器發送事件(SSE),但這些并不完全自然。
使用WebSocket,處理持久性TCP通信,服務器和客戶端都可以相互獨立地發送數據,事實上,對于許多通信方來說!這被稱為雙向通信。
WebSocket通信的另一個有趣特性是它是全雙工的?,F在雖然這個詞可能聽起來很深奧; 它只是意味著服務器和客戶端都可以同時發送數據。將此與HTTP中發生的情況進行比較,其中服務器必須等待它才能完全接收請求,然后才能響應數據。
雖然雙向和全雙工通信的好處可能不會立即顯現出來。我們將看到一些用戶解鎖一些真正的力量的用例。
6.5。安全
最后但同樣重要的是,HTTP和WebSocket都利用了TLS的優勢來提高安全性。雖然HTTP提供HTTPS作為使用該網頁的網址方案的一部分,已經的WebSocket WSS為同樣的效果的URL方案的一部分。
因此,前一小節中的URL的安全版本應如下所示:
https://localhost:443/rest
wss://localhost:443/ws
保護RESTful服務或WebSocket通信是一個非常深入的主題,這里不能涵蓋?,F在,我們只是說兩者在這方面得到了充分的支持。
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小時內與您取得聯系。