項目實戰 — 消息隊列(8){網絡通信設計①}

目錄

一、自定義應用層協議

🍅 1、格式定義

🍅 2、準備工作

🎄定義請求和響應?

?🎄 定義BasicArguments

🎄 定義BasicReturns

🍅 2、創建參數類

????????🎄 交換機

????????🎄 隊列

????????🎄 綁定

? ? ? ? 🎄發布消息

? ? ? ? 🎄 訂閱消息

? ? ? ? 🎄確認應答

? ? ? ? 🎄 消息推送

二、服務器設計

?🍅 1、編寫實例變量和構造方法

🍅 2、編寫啟動類和關閉類

🍅 3、編寫處理連接的方法:processConnection()

?🍅 4、編寫讀取請求readRequest()和寫回響應writeResponse方法

🍅 5、實現根據請求計算響應:process()方法編寫


一、自定義應用層協議

🍅 1、格式定義

本消息隊列,是需要通過網絡進行通信的。這里主要基于TCP協議,自定義應用層協議。

由于當前交互的Message數據,是二進制數據,由于HTTP和JSON都是文本協議,所以這里就不適用了。使用自定義的應用層協議。

約定自定義應用層協議格式:

????????以下是請求和響應的組成部分:

?type:

描述當前請求和響應式做什么的,描述當前請求/響應是在調用哪個API(VirtualHost中的核心API)

????????以下是type標識請求相應不同的功能,取值如下:

????????其中Channel代表的是Connection(TCP的連接)內部的”邏輯上"的連接。此時一個? ? ? ? ? ?Connection中可能會含有多個Channel。存在的意義是為了讓TCP連接

VirtualHost中的十多個方法:
0x1創建channel
0x2關閉channel
0x3創建exchange
0x4銷毀exchange
0x5創建queue
0x6銷毀queue
0x7創建binding
0x8銷毀binding
0x9發送message
0xa訂閱message
0xb返回ack
0xc服務器給客戶端推送的消息(被訂閱的消息)(響應獨有)

length:描述了payload的長度

payload:?會根據當前是請求還是響應,以及當前的type有不同的取值。

比如當前是0x3(創建交換機),

/*
* 表示一個網絡通信中的請求對象,按照自定義協議的格式來展開
* */
@Data
public class Request {private int type;private int length;private byte[] payload;
}

當前是一個請求,那么pyload中的內容是exchangeDeclare的參數的序列化的結果;

如果當前是一個響應,那么payload里面的內容就是exchangeDeclare的返回結果的序列化內容。

那么接下來就進行代碼設計

以下都是再commen包中創建。

🍅 2、準備工作

🎄定義請求和響應?

/*
* 表示一個網絡通信中的請求對象,按照自定義協議的格式來展開
* */
@Data
public class Request {private int type;private int length;private byte[] payload;
}
/*
* 表示一個網絡通信中的響應對象,也是根據自定義應用層協議來的
* */
@Data
public class Response {private int type;private int length;private byte[] payload;
}

?🎄 定義BasicArguments

使用這個類表示方法的公共參數/輔助的字段 ,后續的每個方法會有一些不同的參數,不同的參數再使用不同的子類來表示。

rid代表請求的id,和響應的id一樣,他們是一對

channel表示的是“邏輯連接”,表示客戶端各種模塊復用一個TCP連接,

channelId就代表這些連接。

@Data
public class BasicArguments implements Serializable {
//     表示一次請求/響應的身份標識,可以把請求和響應對上protected String rid;
//    客戶端的身份標識protected String channelId;
}

🎄 定義BasicReturns

使用這個類標識各個遠程調用的方法的返回值的公共信息

/*
* 標識各個遠程調用的方法的返回值的公共信息
* */
@Data
public class BasicReturns implements Serializable {
//    用來標識唯一的請求和響應protected String rid;protected String channelId;
//    用來表示當前遠程調用方法的返回值protected boolean ok;
}

🍅 2、創建參數類

根據前面VirtualHost中的十多個方法,每個方法創建一個類,標識該方法中的相關參數。

那么這個參數到底是如何進行傳遞的?

如下圖,以交換機的參數進行舉例。

關于我們遠程調用的過程:當發起請求時,就把這些參數通過請求傳過去,然后調用VirtualHost中的API(就是VirtualHost中的那些創建刪除方法),調用完以后再返回響應。

以下是有關交換機的請求報文:

以下是創建交換機的響應報文:沒有請求報文復雜是因為,響應只需要返回請求是否執行遠程調用是否成功即可。?

以下就創建這些參數類:?

????????🎄 交換機

?創建交換機:

@Data
public class ExchangeDeclareArguments extends BasicArguments implements Serializable {private String ExchangeName;private ExchangeType exchangeType;private boolean durable;
}

刪除交換機:

@Data
public class ExchangeDeleteArguments extends BasicArguments implements Serializable {private String exchangeName;
}

????????🎄 隊列

創建隊列:

@Data
public class QueueDeclareArguments extends BasicArguments implements Serializable {private String QueueName;private boolean durable;
}

刪除隊列:

@Data
public class QueueDeleteArguments extends BasicArguments implements Serializable {private String queueName;
}

????????🎄 綁定

創建綁定:

@Data
public class QueueBindArguments extends BasicArguments implements Serializable {private String exchangeName;private String queueName;private String bindingKey;
}

刪除綁定:

@Data
public class QueueUnbindArguments extends BasicArguments implements Serializable {private String queueName;private String exchangeName;
}

? ? ? ? 🎄發布消息

@Data
public class BasicPublishArguments extends BasicArguments implements Serializable {private String exchangeName;private String routingKey;private BasicProperties basicProperties;private byte[] body;
}

? ? ? ? 🎄 訂閱消息

這個方法參數,還包含一個Consumer consumer。

這是一個回調函數,這個回調函數是不能作為參數進行傳輸的,因為這個回調函數,是客戶端這邊的。

比如,這里請求調用一個”訂閱隊列“的遠程方法,

客戶端這邊:服務器收到了請求,執行了basicConsume方法,并且返回了響應。訂閱以后,客戶端的消費者就會在后面收到消息,而這個回調函數是在消費者收到消息以后,才會進行邏輯處理,而不是再發送請求時進行傳遞的。

服務器這邊:執行的是一個固定的回調函數:把消息返回給客戶端。

@Data
public class BasicConsumeArguments extends BasicArguments implements Serializable {private String consumerTag;private String queueName;private boolean autoAck;
}

? ? ? ? 🎄確認應答

@Data
public class BasicAckArguments extends BasicArguments implements Serializable {private String queueName;private String messageId;
}

? ? ? ? 🎄 消息推送

前面的都是客戶端給服務器發送消息,這里是服務器給消費者推送消息。所以要繼承BasicReturns。

@Data
public class SubScribeReturns extends BasicReturns implements Serializable {private String consumerTag;private BasicProperties basicProperties;private byte[] body;
}

二、服務器設計

在 mqServer包中創建一個BrokerServer類。

?🍅 1、編寫實例變量和構造方法

 private ServerSocket serverSocket = null;private VirtualHost virtualHost = new VirtualHost("default");//    使用這個哈希表,表示當前所有會話(那些客戶端在和這個服務器進行通信)
//    此處的key是channelId,value是對應的 socket對象private ConcurrentHashMap<String , Socket> sessions = new ConcurrentHashMap<String ,Socket>();//    引入線程池,處理多個客戶端的請求private ExecutorService executorService = null;//    引入boolean變量控制服務器是否運行private volatile boolean runnable = true;public BrokerServer(int port) throws IOException {
//        端口號serverSocket = new ServerSocket(port);}

🍅 2、編寫啟動類和關閉類

?這里利用了線程池,不斷的處理連接

public void start() throws IOException {System.out.println("[BrokerServer] 啟動!");executorService = Executors.newCachedThreadPool();try {while (runnable) {Socket clientSocket = serverSocket.accept();// 把處理連接的邏輯丟給這個線程池.executorService.submit(() -> {processConnection(clientSocket);});}} catch (SocketException e) {System.out.println("[BrokerServer] 服務器停止運行!");// e.printStackTrace();}}public void stop() throws IOException {runnable = false;
//        停止線程池executorService.shutdownNow();serverSocket.close();}private void processConnection(Socket clientSocket) {//TODO
}

🍅 3、編寫處理連接的方法:processConnection()

處理一個客戶端的連接,主要有以下幾步:

? ? ? ? (1)讀取請求并且解析

? ? ? ? (2)根據請求計算響應

? ? ? ? (3)把相應協寫回給客戶端

//    通過該方法,處理一個客戶端的連接
//    在一個連接中,可能會涉及到多個連接和請求private void processConnection(Socket clientSocket) throws IOException {
//        獲取到流對象,讀取應用層協議try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()){
//                按照特定格式來讀取并且解析(轉換),此時就需要用到DataInputStream和DataOutputStreamtry (DataInputStream dataInputStream = new DataInputStream(inputStream);DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {while (true) {
//                  1、讀取請求并且解析Request request = readRequest(dataInputStream);
//                  2、根據請求計算響應Response response = process(request, clientSocket);
//                  3、把響應寫回給客戶端writeResponse(dataOutputStream,response);}}}catch (EOFException|SocketException e) {
//                DataInputStream如果讀到EOF(文件末尾),會拋出一個EOFException異常
//                視為正常的異常,用或者異常來結束循環System.out.println("[BrokerServer] connection 關閉! 客戶端的地址: " + clientSocket.getInetAddress().toString()+ ":" + clientSocket.getPort());} catch (IOException | ClassNotFoundException | MqException e) {System.out.println("[BrokerServer] connection 出現異常!");e.printStackTrace();} finally {try {clientSocket.close();
//          一個TCP連接中,可能含有多個channel,需要把當前socket對應的channel也順便清理掉clearClosedSession(clientSocket);}catch (IOException e) {e.printStackTrace();}}}

?🍅 4、編寫讀取請求readRequest()和寫回響應writeResponse方法

這里就是根據前面設定的報文格式來編寫的讀取請求和寫回響應的方法,這里的payload的具體內容在這里不作解析,在后面的process方法中進行解析

//    讀取請求并且解析private Request readRequest(DataInputStream dataInputStream) throws IOException {Request request = new Request();//        讀取出請求中4個字節的typerequest.setType(dataInputStream.readInt());
//        讀出4個字節的lengthrequest.setLength(dataInputStream.readInt());byte[] payload = new byte[request.getLength()];int n = dataInputStream.read(payload);if (n != request.getLength()){throw new IOException("讀取請求格式出錯");}request.setPayload(payload);return request;}//    把響應寫回給客戶端private void writeResponse(DataOutputStream dataOutputStream, Response response) throws IOException {dataOutputStream.writeInt(response.getType());dataOutputStream.writeInt(response.getLength());dataOutputStream.write(response.getPayload());
//        刷新緩沖區dataOutputStream.flush();}

🍅 5、實現根據請求計算響應:process()方法編寫

這里就要針對具體的payload進行編寫了。

當前請求中的payload里面的內容,是根據type來的,如下

VirtualHost中的十多個方法:
0x1創建channel
0x2關閉channel
0x3創建exchange
0x4銷毀exchange
0x5創建queue
0x6銷毀queue
0x7創建binding
0x8銷毀binding
0x9發送message
0xa訂閱message
0xb返回ack
0xc服務器給客戶端推送的消息(被訂閱的消息)(響應獨有)

如果是0x3,就是創建交換機對應的參數......?

主要分為以下幾步:

? ? ? ? 1、把request中的payload作出一個初步的解析

? ? ? ? 2、根據type的值,進一步區分請求要做什么

? ? ? ? 3、構造響應

private Response process(Request request, Socket clientSocket) throws IOException, ClassNotFoundException, MqException {// 1. 把 request 中的 payload 做一個初步的解析.BasicArguments basicArguments = (BasicArguments) BinaryTool.fromBytes(request.getPayload());System.out.println("[Request] rid=" + basicArguments.getRid() + ", channelId=" + basicArguments.getChannelId()+ ", type=" + request.getType() + ", length=" + request.getLength());// 2. 根據 type 的值, 來進一步區分接下來這次請求要干啥.boolean ok = true;if (request.getType() == 0x1) {// 創建 channelsessions.put(basicArguments.getChannelId(), clientSocket);System.out.println("[BrokerServer] 創建 channel 完成! channelId=" + basicArguments.getChannelId());} else if (request.getType() == 0x2) {// 銷毀 channelsessions.remove(basicArguments.getChannelId());System.out.println("[BrokerServer] 銷毀 channel 完成! channelId=" + basicArguments.getChannelId());} else if (request.getType() == 0x3) {// 創建交換機. 此時 payload 就是 ExchangeDeclareArguments 對象了.ExchangeDeclareArguments arguments = (ExchangeDeclareArguments) basicArguments;ok = virtualHost.exchangeDeclare(arguments.getExchangeName(), arguments.getExchangeType(),arguments.isDurable());} else if (request.getType() == 0x4) {
//        刪除交換機ExchangeDeleteArguments arguments = (ExchangeDeleteArguments) basicArguments;ok = virtualHost.exchangeDelete(arguments.getExchangeName());} else if (request.getType() == 0x5) {
//            創建隊列QueueDeclareArguments arguments = (QueueDeclareArguments) basicArguments;ok = virtualHost.queueDeclare(arguments.getQueueName(), arguments.isDurable());} else if (request.getType() == 0x6) {
//        刪除隊列QueueDeleteArguments arguments = (QueueDeleteArguments) basicArguments;ok = virtualHost.queueDelete((arguments.getQueueName()));} else if (request.getType() == 0x7) {
//            創建綁定QueueBindArguments arguments = (QueueBindArguments) basicArguments;ok = virtualHost.queueBind(arguments.getQueueName(), arguments.getExchangeName(), arguments.getBindingKey());} else if (request.getType() == 0x8) {//    刪除綁定QueueUnbindArguments arguments = (QueueUnbindArguments) basicArguments;ok = virtualHost.queueUnbind(arguments.getQueueName(), arguments.getExchangeName());} else if (request.getType() == 0x9) {BasicPublishArguments arguments = (BasicPublishArguments) basicArguments;ok = virtualHost.basicPublish(arguments.getExchangeName(), arguments.getRoutingKey(),arguments.getBasicProperties(), arguments.getBody());} else if (request.getType() == 0xa) {BasicConsumeArguments arguments = (BasicConsumeArguments) basicArguments;ok = virtualHost.basicConsume(arguments.getConsumerTag(), arguments.getQueueName(), arguments.isAutoAck(),new Consumer() {// 這個回調函數要做的工作, 就是把服務器收到的消息可以直接推送回對應的消費者客戶端@Overridepublic void handleDelivery(String consumerTag, BasicProperties basicProperties, byte[] body) throws MqException, IOException {// 先知道當前這個收到的消息, 要發給哪個客戶端.// 此處 consumerTag 其實是 channelId. 根據 channelId 去 sessions 中查詢, 就可以得到對應的// socket 對象了, 從而可以往里面發送數據了// 1. 根據 channelId 找到 socket 對象Socket clientSocket = sessions.get(consumerTag);if (clientSocket == null || clientSocket.isClosed()) {throw new MqException("[BrokerServer] 訂閱消息的客戶端已經關閉!");}// 2. 構造響應數據SubScribeReturns subScribeReturns = new SubScribeReturns();subScribeReturns.setChannelId(consumerTag);subScribeReturns.setRid(""); // 由于這里只有響應, 沒有請求, 不需要去對應. rid 暫時不需要.subScribeReturns.setOk(true);subScribeReturns.setConsumerTag(consumerTag);subScribeReturns.setBasicProperties(basicProperties);subScribeReturns.setBody(body);byte[] payload = BinaryTool.toBytes(subScribeReturns);Response response = new Response();// 0xc 表示服務器給消費者客戶端推送的消息數據.response.setType(0xc);// response 的 payload 就是一個 SubScribeReturnsresponse.setLength(payload.length);response.setPayload(payload);// 3. 把數據寫回給客戶端.//    注意! 此處的 dataOutputStream 這個對象不能 close !!!//    如果 把 dataOutputStream 關閉, 就會直接把 clientSocket 里的 outputStream 也關了.//    此時就無法繼續往 socket 中寫入后續數據了.DataOutputStream dataOutputStream = new DataOutputStream(clientSocket.getOutputStream());writeResponse(dataOutputStream, response);}});} else if (request.getType() == 0xb) {// 調用 basicAck 確認消息.BasicAckArguments arguments = (BasicAckArguments) basicArguments;ok = virtualHost.basicAck(arguments.getQueueName(), arguments.getMessageId());} else {// 當前的 type 是非法的.throw new MqException("[BrokerServer] 未知的 type! type=" + request.getType());}// 3. 構造響應BasicReturns basicReturns = new BasicReturns();basicReturns.setChannelId(basicArguments.getChannelId());basicReturns.setRid(basicArguments.getRid());basicReturns.setOk(ok);byte[] payload = BinaryTool.toBytes(basicReturns);Response response = new Response();response.setType(request.getType());response.setLength(payload.length);response.setPayload(payload);System.out.println("[Response] rid=" + basicReturns.getRid() + ", channelId=" + basicReturns.getChannelId()+ ", type=" + response.getType() + ", length=" + response.getLength());return response;}

🍅 6、清理過期的sessions:clearClosedSession()

    //    遍歷sessions hash表,把該被關閉的socket對應的鍵值對都刪掉private void clearClosedSession(Socket clientSocket) {List<String> toDeleteChannelId = new ArrayList<>();for(Map.Entry<String,Socket> entry : sessions.entrySet()){if(entry.getValue() == clientSocket){
//                使用集合類,不能一邊遍歷,一邊刪除toDeleteChannelId.add(entry.getKey());}}for (String channelId : toDeleteChannelId){sessions.remove(channelId);}System.out.println("[BrokerServer]清理session完成~ 被清理的channeId = " + toDeleteChannelId);}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/36251.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/36251.shtml
英文地址,請注明出處:http://en.pswp.cn/news/36251.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【網絡】傳輸層——TCP(滑動窗口流量控制擁塞控制延遲應答捎帶應答)

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;專欄&#xff1a;《網絡》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交給時間&#xff01; 上篇文章對TCP可靠性機制講解了一部分&#xff0c;這篇文章接著繼續講解。 &#x1f3a8;滑動窗口 在…

Springboot 實踐(2)MyEclipse2019創建項目修改pom文件,加載springboot 及swagger-ui jar包

MyEclipse2019創建工程之后&#xff0c;需要添加Springboot啟動函數、添加application.yml配置文件、修改pom文件添加項目使用的jar包。 添加Springboot啟動函數 創建文件存儲路徑 &#xff08;1&#xff09;右鍵單擊“src/main/java”文件夾&#xff0c;彈出對話框輸入路徑…

Android 簡單的視頻、圖片壓縮工具

首頁需要壓縮的工具包 1.Gradle implementation com.iceteck.silicompressorr:silicompressor:2.2.3 2.添加相關權限&#xff08;手機得動態申請權限&#xff09; <uses-permission android:name"android.permission.READ_EXTERNAL_STORAGE"/> <uses-p…

05 - 研究 .git 目錄

查看所有文章鏈接&#xff1a;&#xff08;更新中&#xff09;GIT常用場景- 目錄 文章目錄 1. HEAD2. config3. refs4. objects 1. HEAD 2. config 3. refs 4. objects Git對象一共有三種&#xff1a;數據對象 blob、樹對象 tree以及提交對象 commit&#xff0c;這些對象都被保…

Vue 目錄結構 vite 項目

Vue3 項目常用的目錄結構和每個文件的作用【通過 vite 創建的項目】 vite目錄結構&#xff1a; dist // 打包后生成的文件目錄 node_modules // 環境依賴 public // 公共資源目錄 favicon.ico …

深入探析設計模式:工廠模式的三種姿態

深入探析設計模式&#xff1a;工廠模式的三種姿態 1. 簡單工廠模式1.1 概念1.2 案例1.3 優缺點 2. 抽象工廠模式2.1 概念2.2 案例&#xff1a;跨品牌手機生產2.3 優缺點 3. 超級工廠模式3.1 概念3.2 案例&#xff1a;動物園游覽3.3 優缺點 4. 總結 歡迎閱讀本文&#xff0c;今天…

go入門實踐四-go實現一個簡單的tcp-socks5代理服務

文章目錄 前言socks協議簡介go實現一個簡單的socks5代理運行與壓測抓包驗證 前言 SOCKS是一種網絡傳輸協議&#xff0c;主要用于客戶端與外網服務器之間通訊的中間傳遞。協議在應用層和傳輸層之間。 本文使用先了解socks協議。然后實現一個socks5的tcp代理服務端。最后&#…

英語詞法——代詞

代詞是用來代替名詞、起名詞作用的短語、分句和句子的詞。英語中代詞根據其意義和作用可分為九類:人稱代詞、物主代詞、反身代詞、相互代詞、指示代詞、疑問代詞、不定代詞、關系代詞和連接代詞。 第一節 人稱代詞 一、人稱代詞的形式和用法 人稱代詞單數復數第一人稱第二人…

【ARM 嵌入式 編譯系列 4 -- GCC 編譯屬性 __read_mostly 詳細介紹】

文章目錄 __read_mostly 介紹__read_mostly 在 linux 中的使用.data.read_mostly 介紹 __read_mostly 介紹 __read_mostly 是一個在Linux內核編程中用到的宏定義&#xff0c;這是一個gcc編譯器的屬性&#xff0c;用于告訴編譯器此變量主要用于讀取&#xff0c;很少進行寫入&am…

MYSQL中用字符串2022-07去匹配Date類型大于2022-07-01并小于2022-07-31

正文 需求上&#xff0c;是有個日期字符串&#xff0c;例如2022-07&#xff0c;代表著年月。數據庫中表對于這個字段存的是年月日&#xff0c;例如&#xff1a;2022-07-15。 我希望的是&#xff1a;獲取到2022-07-01到2022-07-31&#xff0c;之間的數據&#xff0c;條件是&…

21款美規奔馳GLS450更換中規高配主機,漢化操作更簡單

很多平行進口的奔馳GLS都有這么一個問題&#xff0c;原車的地圖在國內定位不了&#xff0c;語音交互功能也識別不了中文&#xff0c;原廠記錄儀也減少了&#xff0c;使用起來也是很不方便的。 可以實現以下功能&#xff1a; ①中國地圖 ②語音小助手&#xff08;你好&#xf…

【BASH】回顧與知識點梳理(二十六)

【BASH】回顧與知識點梳理 二十六 二十六. 二十一至二十五章知識點總結及練習26.1 總結26.2 模擬26.3 簡答題 該系列目錄 --> 【BASH】回顧與知識點梳理&#xff08;目錄&#xff09; 二十六. 二十一至二十五章知識點總結及練習 26.1 總結 Linux 操作系統上面&#xff0c…

unittest單元測試

當你在編寫測試用例時&#xff0c;可以使用Python內置的unittest模塊來進行單元測試。下面是一個逐步指南&#xff0c;幫助你理解如何編寫和運行基本的單元測試。 導入必要的模塊&#xff1a; 首先&#xff0c;你需要導入unittest模塊和需要測試的模塊&#xff08;例如&#xf…

運維監控學習筆記8

在服務器端&#xff0c;我們添加了nginx-server的主機&#xff1a; 在解決Error問題的過程中&#xff0c;我還通過zabbix_get這個命令進行了測試&#xff0c;發現是沒有的&#xff0c;后來確認是在web頁面配置的過程中&#xff0c;我輸錯了密碼。 yum install zabbix-getzabbi…

uniapp-原生地圖截屏返回base64-進行畫板編輯功能

一、場景 vue寫uniapp打包安卓包&#xff0c;實現原生地圖截屏&#xff08;andirod同事做的&#xff09;-畫板編輯功能 實現效果&#xff1a; 二、邏輯步驟簡略 1. 由 原生地圖nvue部分&#xff0c;回調返回 地圖截屏生成的base64 數據&#xff0c; 2. 通過 uni插件市場 im…

《圖解HTTP》——HTTP協議詳解

一、HTTP協議概述 HTTP是一個屬于應用層的面向對象協議&#xff0c;由于其簡捷、快速的方式&#xff0c;適用于分布式超媒體信息系統。它于1990年提出&#xff0c;經過幾年的使用與發展&#xff0c;得到不斷地完善和擴展。目前在WWW中使用的是HTTP/1.0的第六版&#xff0c;HTTP…

muduo 29 異步日志

目錄 Muduo雙緩沖異步日志模型: 異步日志實現: 為什么要實現非阻塞的日志

SQL 語句解析過程詳解

SQL 語句解析過程詳解&#xff1a; 1&#xff0e;輸入SQL語句 2&#xff0e;詞法分析------flex 使用詞法分析器&#xff08;由Flex生成&#xff09;將 SQL 語句分解為一個個單詞&#xff0c;這些單詞被稱為“標記“。標記包括關鍵字、標識符、運算符、分隔符等。 2.1 flex 原…

【CSS 布局】水平垂直方向居中

【CSS 布局】水平垂直方向居中 單行元素 <div class"container"><div class"item"></div> </div>方式一&#xff1a;relative 和 absolute .container {position: relative;height: 400px;border: 1px solid #ccc;.item {posit…

20個互聯網用戶Python數據分析項目

這篇文章給大家整理了20個互聯網用戶數據分析的項目。所有收錄的項目&#xff0c;進行了嚴格的篩選&#xff0c;標準有二&#xff1a; 1.有解說性文字&#xff0c;大家能知道每一步在干嘛&#xff0c;新手友好 2.數據集公開&#xff0c;保證大家可以在原文的基礎上自行探索 更…