day01-從一個基礎的socket服務說起

来源:https://www.cnblogs.com/huiwancode/archive/2022/04/25/16187820.html
-Advertisement-
Play Games

首發地址:day01-從一個基礎的socket服務說起 教程說明:C++高性能網路服務保姆級教程 本節目的 實現一個基於socket的echo服務端和客戶端 服務端監聽流程 第一步:使用socket函數創建套接字 在linux中,一切都是文件,所有文件都有一個int類型的編號,稱為文件描述符。服務端 ...


首發地址:day01-從一個基礎的socket服務說起

教程說明:C++高性能網路服務保姆級教程

本節目的

實現一個基於socket的echo服務端和客戶端

服務端監聽流程

第一步:使用socket函數創建套接字

在linux中,一切都是文件,所有文件都有一個int類型的編號,稱為文件描述符。服務端和客戶端通信本質是在各自機器上創建一個文件,稱為socket(套接字),然後對該socket文件進行讀寫。

在 Linux 下使用 <sys/socket.h> 頭文件中 socket() 函數來創建套接字

int socket(int af, int type, int protocol);
  • af: IP地址類型; IPv4填AF_INET, IPv6填AF_INET6
  • type: 數據傳輸方式, SOCK_STREAM表示流格式、面向連接,多用於TCP。SOCK_DGRAM表示數據報格式、無連接,多用於UDP
  • protocol: 傳輸協議, IPPROTO_TCP表示TCP。IPPTOTO_UDP表示UDP。可直接填0,會自動根據前面的兩個參數自動推導協議類型
#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

第二步:使用bind函數綁定套接字和監聽地址

socket()函數創建出套接字後,套接字中並沒有任何地址信息。需要用bind()函數將套接字和監聽的IP和埠綁定起來,這樣當有數據到該IP和埠時,系統才知道需要交給綁定的套接字處理。

bind函數也在<sys/socket.h>頭文件中,原型為:

int bind(int sock, struct sockaddr *addr, socklen_t addrlen);
  • sock: socket函數返回的socket描述符
  • addr:一個sockaddr結構體變數的指針,後續會展開說。
  • addrlen:addr的大小,直接通過sizeof得到

我們先看看socket和bind的綁定代碼,下麵代碼中,我們將創建的socket與ip='127.0.0.1',port=8888進行綁定:

#include <sys/socket.h>
#include <netinet/in.h>

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));  //用0填充
server_addr.sin_family = AF_INET;  //使用IPv4地址
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具體的IP地址;填入INADDR_ANY表示"0.0.0.0"
server_addr.sin_port = htons(8888);  //埠
//將套接字和IP、埠綁定
bind(server_addr, (struct sockaddr*)&server_addr, sizeof(server_addr));

可以看到,我們使用sockaddr_in結構體設置要綁定的地址信息,然後再強制轉換為sockaddr類型。這是為了讓bind函數能適應多種協議。

struct sockaddr_in{
    sa_family_t     sin_family;   //地址族(Address Family),也就是地址類型
    uint16_t        sin_port;     //16位的埠號
    struct in_addr  sin_addr;     //32位IP地址
    char            sin_zero[8];  //不使用,一般用0填充
};

struct sockaddr_in6 { 
    sa_family_t sin6_family;  //(2)地址類型,取值為AF_INET6
    in_port_t sin6_port;  //(2)16位埠號
    uint32_t sin6_flowinfo;  //(4)IPv6流信息
    struct in6_addr sin6_addr;  //(4)具體的IPv6地址
    uint32_t sin6_scope_id;  //(4)介面範圍ID
};

struct sockaddr{
    sa_family_t  sin_family;   //地址族(Address Family),也就是地址類型
    char         sa_data[14];  //IP地址和埠號
};

其中,sockaddr_in是保存IPv4的結構體;sockadd_in6是保存IPv6的結構體;sockaddr是通用的結構體,通過將特定協議的結構體轉換成sockaddr,以達到bind可綁定多種協議的目的。

註意在設置server_addr的埠號時,需要使用htons函數將傳進來的埠號轉換成大端位元組序

電腦硬體有兩種儲存數值的方式:大端位元組序和小端位元組序
大端位元組序指數值的高位位元組存在前面(低記憶體地址),低位位元組存在後面(高記憶體地址)。
小端位元組序則反過來,低位位元組存在前面,高位位元組存在後面。

電腦電路先處理低位位元組,效率比較高,因為計算都是從低位開始的。而電腦讀記憶體數據都是從低地址往高地址讀。所以,電腦的內部是小端位元組序。但是,人類還是習慣讀寫大端位元組序。除了電腦的內部處理,其他的場合比如網路傳輸和文件儲存,幾乎都是用的大端位元組序。

linux在頭文件<arpa/inet.h>提供了htonl/htons用於將數值轉化為網路傳輸使用的大端位元組序儲存;對應的有ntohl/ntohs用於將數值從網路傳輸使用的大端位元組序轉化為電腦使用的位元組序

第三步:使用listen函數讓套接字進入監聽狀態

int listen(int sock, int backlog);  //Linux
  • backlog:表示全連接隊列的大小

半連接隊列&全連接隊列:我們都知道tcp的三次握手,在第一次握手時,服務端收到客戶端的SYN後,會把這個連接放入半連接隊列中。然後發送ACK+SYN。在收到客戶端的ACK回包後,握手完成,會把連接從半連接隊列移到全連接隊列中,等待處理。

第四步:調用accept函數獲取客戶端請求

調用listen後,此時客戶端就可以和服務端三次握手建立連接了,但建立的連接會被放到全連接隊列中。accept就是從這個隊列中獲取客戶端請求。每調用一次accept,會從隊列中獲取一個客戶端請求。

int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
  • sock:服務端監聽的socket
  • addr:獲取到的客戶端地址信息

accpet返回一個新的套接字,之後服務端用這個套接字與連接對應的客戶端進行通信。

在沒請求進來時調用accept會阻塞程式,直到新的請求進來。

至此,我們就講完了服務端的監聽流程,接下來我們可以先調用read等待讀入客戶端發過來的數據,然後再調用write向客戶端發送數據。再用close把accept_fd關閉,斷開連接。完整代碼如下

// server.cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <cstdio>
#include <errno.h>

int main() {
  int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
  struct sockaddr_in server_addr;
  bzero(&server_addr, sizeof(server_addr));
  server_addr.sin_family = AF_INET;
  server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
  server_addr.sin_port = htons(8888);
  if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
    printf("bind err: %s\n", strerror(errno));
    close(listen_fd);
    return -1;
  }

  if (listen(listen_fd, 2048) < 0) {
    printf("listen err: %s\n", strerror(errno));
    close(listen_fd);
    return -1;
  }
  
  struct sockaddr_in client_addr;
  bzero(&client_addr, sizeof(struct sockaddr_in));
  socklen_t client_addr_len = sizeof(client_addr);
  int accept_fd = 0;
  while((accept_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr_len)) > 0) {
    printf("get accept_fd: %d from: %s:%d\n", accept_fd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
    
    char read_msg[100];
    int read_num = read(accept_fd, read_msg, 100);
    printf("get msg from client: %s\n", read_msg);
    int write_num = write(accept_fd, read_msg, read_num);
    close(accept_fd);
  }
}

[C++小知識] 在使用printf列印調試信息時,由於系統緩衝區問題,如果不加"\n",有時會列印不出來字元串。

C提供的很多函數調用產生錯誤時,會將錯誤碼賦值到一個全局int變數errno上,可以通過strerror(errno)輸入具體的報錯信息

客戶端建立連接

客戶端就比較簡單了,創建一個sockaddr_in變數,填充服務端的ip和埠,通過connect調用就可以獲取到一個與服務端通信的套接字。

int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);

各個參數的說明和bind()相同,不再重覆。

創建連接後,我們先調write向服務端發送數據,再調用read等待讀入服務端發過來的數據,然後調用close斷開連接。完整代碼如下:

// client.cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <cstdio>
#include <iostream>

int main() {
  int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
  struct sockaddr_in server_addr;
  bzero(&server_addr, sizeof(server_addr));
  server_addr.sin_family = AF_INET;
  server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
  server_addr.sin_port = htons(8888);
  if (connect(sock_fd, (sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
    printf("connect err: %s\n", strerror(errno));
    return -1;
  };
  
  printf("success connect to server\n");
  char input_msg[100];
  // 等待輸入數據
  std::cin >> input_msg;
  printf("input_msg: %s\n", input_msg);
  int write_num = write(sock_fd, input_msg, 100);
  char read_msg[100];
  int read_num = read(sock_fd, read_msg, 100);
  printf("get from server: %s\n", read_msg);
  close(sock_fd);
}

分別編譯後,我們就得到了一個echo服務的服務端和客戶端

~# ./server 
get accept_fd: 4 from: 127.0.0.1:56716
get msg from client: abc
~# ./client 
abc
input_msg: abc
get from server: abc

完整源碼已上傳到CProxy-tutorial,歡迎fork and star!

思考題

先啟動server,然後啟動一個client,不輸入數據,這個時候在另外一個終端上再啟動一個client,併在第二個client終端中輸入數據,會發生什麼呢?

如果本文對你有用,點個贊再走吧!或者關註我,我會帶來更多優質的內容。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 今天的是Python第三話,前面的知識點給大家放在上面了,零基礎的小伙伴可以自己動手領取,學好Python的基礎知識對我們後期 去實現Python案例幫助很大,知其然才能更好解決問題,話不多說,直接開始了。 函數 Python學習交流Q群:906715085#### print(" 定義函數 "); ...
  • 第二話來了 這一章的知識緊接上一章,零基礎的小伙伴可以從上 一章學起來。當然,你也可以收藏起來慢慢學習,學習是不可操之過急的啦… 列表 Python學習交流Q群:906715085### print(" 創建列表 "); list1 = ['JAVA', 'Hello', 'Python', 'VS ...
  • 綜合前述的類、函數、matplotlib等,完成一個隨機移動的過程(註意要確定移動的次數,比如10萬次),每次行走都完全是隨機的,沒有明確的方向,結果是由一系列隨機決策確定的,最後顯示出每次移動的位置的圖表。 思考: 1)每次走動多少個像素,由隨機函數決定,每次移動方向也隨機確定。由隨機方向和隨機像 ...
  • 作者:雙子孤狼 來源:https://blog.csdn.net/zwx900102/article/details/115446997 任何一個服務如果沒有監控,那就是兩眼一抹黑,無法知道當前服務的運行情況,也就無法對可能出現的異常狀況進行很好的處理,所以對任意一個服務來說,監控都是必不可少的。 ...
  • 1.開發環境 新建一個工程-配置jdk版本-配置maven 2.創建maven工程 修改打包方式為jar 導入依賴: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" x ...
  • 在前後端分離的項目中後端返回的格式一定要友好,不然會對前端的開發人員帶來很多的工作量。那麼SpringBoot如何做到統一的後端返回格式呢?今天我們一起來看看。 為什麼要對SpringBoot返回統一的標準格式 在預設情況下,SpringBoot的返回格式常見的有三種: 返回String @GetM ...
  • Python基礎知識 今天給大家分享一些Python的基礎知識,想要蓋好大房子,不把地基打扎實打牢怎麼行呢?所以,今天咱們就來學習基礎知識, 這樣後期學習Python的時候才能更容易掌握,更輕鬆的學會Python的使用。別跟我說你都學過了,看完再告訴我… 一、編程基礎 1.基本的輸入輸出: prin ...
  • 最近一個粉絲說,他面試了4個公司,有三個公司問他:“Spring Boot 中自動裝配機制的原理” 他回答了,感覺沒回答錯誤,但是怎麼就沒給offer呢? 對於這個問題,看看普通人和高手該如何回答。 普通人: 嗯… Spring Boot裡面的自動裝配,就是@EnableAutoConfigurat ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...