Linux下的Socket編程(主要包括TCP部分)

Linux下的Socket編程(主要包括TCP部分)

轉(zhuǎn)載麻煩注明原文地址
本文是Linux下基本的Socket編程進(jìn)行介紹,主要包括以下知識(shí)點(diǎn)

  • Linux下的一些小知識(shí)(編譯常識(shí),文件相關(guān)操作基礎(chǔ),進(jìn)程與線程)
  • TCP網(wǎng)絡(luò)編程基礎(chǔ)(TCP Socket)
  • 基于UDP協(xié)議的接受和發(fā)送(UDP Socket),以及網(wǎng)絡(luò)中的大端存儲(chǔ),小端存儲(chǔ)將在后一篇文章介紹
  • 1.Linux下一些小知識(shí)

這些基礎(chǔ)摘自《Linux網(wǎng)絡(luò)編程(第二版)》,僅是對(duì)個(gè)人的一些Linux常識(shí)補(bǔ)充,讀者可選擇性忽略,直接進(jìn)入網(wǎng)絡(luò)編程環(huán)節(jié)。

(1).程序編譯常識(shí)在Linux中,程序采用的是最廣泛的是GCC編譯,程序從源代碼文件到指定的可執(zhí)行文件從要經(jīng)歷一系列過程,本段將對(duì)這段過程做個(gè)概述。先看一張圖:

編譯順序圖.png

由于本文所涉及到的socket編程都是C語言下的Socket編程,故最開始的代碼源文件都是.c文件。源文件,目標(biāo)文件和可執(zhí)行文件是編譯過程中常用到的名詞。
源文件通常是指存放可編輯代碼的文件。目標(biāo)文件是指經(jīng)過編譯器的編譯生成的CPU可識(shí)別的二進(jìn)制代碼,但是目標(biāo)文件不能用與執(zhí)行,因?yàn)槠渲械囊恍┖瘮?shù)過程沒有相關(guān)的指示和說明。可執(zhí)行文件就是目標(biāo)文件與相關(guān)鏈接庫(kù)鏈接后的文件,是可以執(zhí)行的。
??預(yù)編譯過程:預(yù)編譯過程采用的是" gcc -E 文件名.c"命令的形式將源文件進(jìn)行預(yù)編譯,該過程的目的是將程序中引用的頭文件包含進(jìn)源代碼中,并對(duì)一些宏進(jìn)行替換,生成后綴名為.i的中間文件。
??編譯過程:編譯過程將用戶可識(shí)別的語言翻譯成一組處理器可識(shí)別的操作碼,通常翻譯成匯編語言。匯編語言通常和機(jī)器操作碼是一對(duì)一的關(guān)系,編譯過程生成匯編語言的GGC 選項(xiàng)是-S,生成后綴名為.s的匯編文件。
??匯編過程:匯編過程當(dāng)然就好理解了,就是講匯編文件使用GCC選項(xiàng)-C進(jìn)行匯編,將匯編文件翻譯成機(jī)器可以識(shí)別的機(jī)器操作碼,也就是后綴名是.obj或者.o目標(biāo)文件。
??鏈接過程:所有目標(biāo)文件必須通過某種方式組合起來才能運(yùn)行,目標(biāo)文件中僅解析了文件內(nèi)部的變量和函數(shù),對(duì)于引用的函數(shù)和變量還沒有解析,這就需要將其他已經(jīng)編寫好的文件引用進(jìn)來,對(duì)沒有解析的變量和函數(shù)進(jìn)行解析,通常引用的目標(biāo)是庫(kù),鏈接完成之后生成文件名為a.out的可執(zhí)行文件。
??上述小結(jié):上述過程僅僅是對(duì)在Linux下使用GCC編譯器編譯的各個(gè)過程分開詳細(xì)描述,對(duì)于單個(gè).C的源文件,直接使用gcc命令加上要變異的C語言源文件,GCC會(huì)自動(dòng)生成文件名為a.out的可執(zhí)行文件,自動(dòng)的過程包括了頭文件擴(kuò)展,目標(biāo)文件編譯,以及鏈接默認(rèn)的系統(tǒng)庫(kù)一些列操作,最后生成系統(tǒng)默認(rèn)的可執(zhí)行程序a.out。如:gcc hello.c,就會(huì)直接編譯成可執(zhí)行文件。
(2).鏈接庫(kù)與加載庫(kù)
??靜態(tài)鏈接庫(kù):靜態(tài)庫(kù)是obj文件的集合,通常靜態(tài)庫(kù)以“.a”為后綴,靜態(tài)庫(kù)由程序ar生成。靜態(tài)庫(kù)的一個(gè)優(yōu)點(diǎn)或者說作用就是可以在不用重新編譯程序庫(kù)代碼的情況下進(jìn)行程序重新鏈接,這種方法大大節(jié)省了編譯過程的時(shí)間。但是由于現(xiàn)在計(jì)算機(jī)系統(tǒng)的日益強(qiáng)大,編譯的時(shí)間已經(jīng)不是問題;靜態(tài)庫(kù)的另一個(gè)優(yōu)勢(shì)便是可以提供庫(kù)文件給使用人員,而不用公開源代碼。
??動(dòng)態(tài)鏈接庫(kù):動(dòng)態(tài)鏈接庫(kù)是程序運(yùn)行時(shí)加載的庫(kù),當(dāng)動(dòng)態(tài)鏈接庫(kù)正確安裝之后,所有程序都可以使用動(dòng)態(tài)庫(kù)來運(yùn)行程序,動(dòng)態(tài)鏈接庫(kù)是目標(biāo)文件的集合,但目標(biāo)文件在動(dòng)態(tài)鏈接庫(kù)中的組織方式是按照特殊的方式形成的。
??靜態(tài)加載庫(kù):動(dòng)態(tài)加載庫(kù)和一般的動(dòng)態(tài)鏈接所不同的是,一般動(dòng)態(tài)鏈接庫(kù)在程序啟動(dòng)的時(shí)候就要尋找動(dòng)態(tài)庫(kù),找到庫(kù)函數(shù);而動(dòng)態(tài)加載庫(kù)可以用程序的方法來控制什么時(shí)候加載。動(dòng)態(tài)加載庫(kù)主要有函dlopen(),dlerror(),dlsym()和dlclose(),具體使用方式,本文不做過多介紹,網(wǎng)上資源豐富,請(qǐng)讀者自行百度。
(3).Linux下文件相關(guān)基礎(chǔ)
??Linux下文件的內(nèi)涵:文件系統(tǒng)狹義的概念是一種對(duì)存儲(chǔ)設(shè)備上的數(shù)據(jù)進(jìn)行組織和控制的機(jī)制,在Linux下(當(dāng)然包括UNIX),文件的含義比較廣泛,文件的概念不僅僅包括通常意義的保存在磁盤上的各種各樣的數(shù)據(jù),還包括各種各樣的數(shù)據(jù),如鼠標(biāo),鍵盤,網(wǎng)卡,標(biāo)準(zhǔn)輸入輸出等。“一切皆文件”
??文件描述符:在Linux下用文件描述符來表示設(shè)備文件和普通文件。文件描述符是一個(gè)整型的數(shù)據(jù),所有對(duì)文件的操作都通過文件描述符實(shí)現(xiàn)。文件描述符的范圍是0~OPEN_MAX。??文件描述符是文件系統(tǒng)中連接用戶空間和內(nèi)核空間的樞紐,當(dāng)打開一個(gè)或者創(chuàng)建一個(gè)文件時(shí),內(nèi)核空間創(chuàng)建相應(yīng)的結(jié)構(gòu),并生成一個(gè)整形變量傳遞給用戶空間的對(duì)應(yīng)進(jìn)程,進(jìn)程用這個(gè)文件描述符來對(duì)文件操作
??在Linux系統(tǒng)中有3個(gè)已經(jīng)分配的文件描述符,即標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤,它們文件描述符的值分別為0、1和2。
??文件相關(guān)操作:open(),create(),close(),read(),write(),lseek(),這里就不詳細(xì)介紹每個(gè)函數(shù)的用法了,讀者請(qǐng)自行百度。知道的請(qǐng)忽略該內(nèi)容,這部分內(nèi)容有助于理解后面的TCP Socket內(nèi)容。

  • 2.TCP網(wǎng)絡(luò)編程基礎(chǔ)(TCP Socket)
    (1).套接字相關(guān)基礎(chǔ)知識(shí)
    ??套接字地址結(jié)構(gòu):套接字編程需要指定套接字地址作為參數(shù)(套接字不是套接字地址,不要搞混,后面會(huì)有介紹),不同的協(xié)議族有不同的地址結(jié)構(gòu),這些地址結(jié)構(gòu)通常以sockaddr_作為開頭,并且每一個(gè)協(xié)議族有一個(gè)唯一的后綴,例如對(duì)于以太網(wǎng),其結(jié)構(gòu)名稱就是sockaddr_in。
  • 通用套接字?jǐn)?shù)據(jù)結(jié)構(gòu):

sa_family_t sa_family; /協(xié)議族/ char sa_data[14]; /協(xié)議族數(shù)據(jù)/ } ```

  • 實(shí)際使用的套接字結(jié)構(gòu):(比如在以太網(wǎng)中)

u8 sin_len /結(jié)構(gòu)struct socket_in 的長(zhǎng)度,16/
u8 sin_family /通常為AF_INT 協(xié)議族/
u16 sin_port /16為的端口號(hào),網(wǎng)絡(luò)字節(jié)序/
struct in_addr sin_addr /IP地址32位/
char sin_zero[8] /未用/ };

struct in_addr { u32 s_addr /* 32位IP地址,為網(wǎng)絡(luò)字節(jié)序*/ }```

  • 結(jié)構(gòu)sockaddr 和結(jié)構(gòu) sockaddr_in的關(guān)系
    兩種結(jié)構(gòu)關(guān)系圖.png
    由于以上兩種結(jié)構(gòu)的大小是完全一致的,所以在進(jìn)行地址結(jié)構(gòu)設(shè)置時(shí),通常的方法是利用結(jié)構(gòu)struct sockaddr_in進(jìn)行設(shè)置,然后強(qiáng)制轉(zhuǎn)化為struct sockaddr類型,因?yàn)閮蓚€(gè)結(jié)構(gòu)大小完全一致,所以這樣的轉(zhuǎn)換不會(huì)有副作用。
    (2).TCP網(wǎng)絡(luò)編程流程?? 總的來說,TCP網(wǎng)絡(luò)編程有兩端,服務(wù)端創(chuàng)建一個(gè)服務(wù)程序,等待客戶端用戶連接,接收到用戶的連接請(qǐng)求之后,根據(jù)用戶的請(qǐng)求進(jìn)行處理;客戶端則根據(jù)目的服務(wù)器的地址和端口進(jìn)行連接,向服務(wù)端發(fā)送請(qǐng)求,并對(duì)服務(wù)器的響應(yīng)進(jìn)行數(shù)據(jù)處理。先總體看看兩者是如何交互的:
    TCP C&S交互過程.png
    由圖可以看到對(duì)于服務(wù)器來說流程主要分為套接字初始化(socket()),套接字與端口的綁定(bind()),設(shè)置服務(wù)器的偵聽連接(listen()),接受客戶端連接(accept()),接收和發(fā)送數(shù)據(jù)(read()、write())并進(jìn)行數(shù)據(jù)處理及處理完畢的套接字關(guān)閉(close()),而對(duì)于客戶端來說分為套接字初始化(socket()),連接服務(wù)器(connect()),讀寫網(wǎng)絡(luò)數(shù)據(jù)(read()、write())并進(jìn)行數(shù)據(jù)處理和最后的套接字關(guān)閉(close())過程。所以兩者的區(qū)別在與客戶端在創(chuàng)建了套接字之后不進(jìn)行地址綁定(不要著急,后面會(huì)介紹地址綁定),而是直接連接服務(wù)器端。下面詳細(xì)介紹各個(gè)函數(shù):
    (3).TCP網(wǎng)絡(luò)編程流程函數(shù)詳解
  • socket()?函數(shù)原型如下:

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

socket()函數(shù)建立一個(gè)協(xié)議族為domain、協(xié)議類型為type、協(xié)議編號(hào)為protocol的套接字文件描述符。如果函數(shù)調(diào)用成功,會(huì)返回一個(gè)表示這個(gè)套接字的文件描述符,失敗的時(shí)候返回–1。?? 參數(shù)domain用于設(shè)置網(wǎng)絡(luò)通信的域,函數(shù)socket()根據(jù)這個(gè)參數(shù)選擇通信的協(xié)議族,通信的協(xié)議族在文件sys/socket.h定義,包含下表所示的值,以太網(wǎng)應(yīng)該設(shè)置PF_INET這個(gè)域,在程序設(shè)計(jì)的過程中會(huì)發(fā)現(xiàn)有的代碼使用了AF_INET這個(gè)值,在頭文件中AF_INET和PF_INET的值是一致的。


domain的值及其含義.png

??參數(shù)type用于設(shè)置套接字通信的類型,主要有SOCK_STREAM(流式套接字),SOCK_DGRAM(數(shù)據(jù)包套接字)。其余類型如下圖所示:


type的值及其含義.png
函數(shù)socket()并不總是執(zhí)行成功,有可能會(huì)出現(xiàn)錯(cuò)誤,錯(cuò)誤的原因有很多種,可以通過errno獲得。在TCP中可以通過socket(AF_INET,SOCK_STREAM,0)返回一個(gè)TCP的套接字文件操作符。
  • bind()?函數(shù)原型如下:

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);

bind()函數(shù)有三個(gè)參數(shù),第一個(gè)參數(shù)sockfd是用socket()函數(shù)創(chuàng)建的文件操作符,第二個(gè)參數(shù)my_addr是指向一個(gè)struct sockaddr參數(shù)的指針,sockaddr中包含了地址,端口,以及IP地址的信息,在進(jìn)行地址綁定的時(shí)候,需要先將地址結(jié)構(gòu)中的IP地址,端口,類型等結(jié)構(gòu)struct sockaddr中的域進(jìn)行設(shè)置之后才能綁定,這樣進(jìn)行綁定之后才能將套接字文件描述符與地址等結(jié)合在一起。第三個(gè)參數(shù)addrlen是my_addr結(jié)構(gòu)的長(zhǎng)度,可以設(shè)置為sizeof(struct sockaddr)最后,bind()函數(shù)返回值為0表示綁定成功,返回-1表示失敗,同樣失敗時(shí)可以通過查看errno來查看原因。

  • listen()函數(shù)原型:
int listen(int sockfd, int backlog);```

函數(shù)中的sockfd表示當(dāng)前監(jiān)聽的套接字的文件描述符,很容易理解。backlog表示在accept()函數(shù)處理之前在等待隊(duì)列中的客戶端的長(zhǎng)度,如果超過這個(gè)長(zhǎng)度,客戶端就會(huì)返回一個(gè)ECONNREFUSED錯(cuò)誤。成功運(yùn)行時(shí),返回值為0,當(dāng)運(yùn)行失敗時(shí),返回值為-1,并且設(shè)置erno值。注:listen函數(shù)僅僅對(duì)類型為SOCK_STREAM或者SOCK_SEQPACKET的協(xié)議有效

  • accept()函數(shù)原型:

#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

當(dāng)一個(gè)客戶端的連接請(qǐng)求到大服務(wù)器主機(jī)監(jiān)聽的端口時(shí),此時(shí)客戶端的連接會(huì)在隊(duì)列中等待,直到服務(wù)器處理用戶請(qǐng)求。函數(shù)accept()成功執(zhí)行之后,會(huì)返回一個(gè)新的套接字描述符來表示客戶端的連接,客戶端連接的信息可以通過這個(gè)新的描述符來獲得,因此當(dāng)服務(wù)器成功處理客戶端的請(qǐng)求連接后,會(huì)有兩個(gè)文件描述符,老的文件描述符表示當(dāng)前服務(wù)端正在監(jiān)聽的那個(gè)socket,新產(chǎn)生的描述符表示客戶端的連接,使用這個(gè)新的描述符就可以執(zhí)行下面要提到的文件傳輸?shù)裙ぷ髁恕??參數(shù)介紹:當(dāng)accept函數(shù)成功執(zhí)行之后,客戶端的信息就可以通過上面的addr來獲得了,其中包括了客戶端的IP,端口,協(xié)議族等內(nèi)容。第三個(gè)參數(shù)表示的是第二個(gè)參數(shù)的長(zhǎng)度,同理,也可以使用sizeof(struct sockaddr_in)獲得。注意:在accept中,addrlen參數(shù)是一個(gè)指針而不是一個(gè)結(jié)構(gòu)體。函數(shù)返回值表示其成功與否,同理,-1表示失敗,errno依然可以獲得其失敗的原因。- 對(duì)客戶端來說的connect()函數(shù)??客戶端在建立套接字之后,不需要進(jìn)行地址綁定就可以直接連接服務(wù)器,連接服務(wù)器的函數(shù)就是connect(),此函數(shù)需要指定參數(shù)服務(wù)器,例如IP地址,端口等。函數(shù)原型如下:

#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

在客戶端,這里的sockfd便指的是客戶端建立套接字返回的文件描述符,第二個(gè)參數(shù)是一個(gè)指向struct sockaddr這種結(jié)構(gòu)體的一個(gè)指針,里面包含了客戶端需要連接服務(wù)器的目的端口,IP地址,以及協(xié)議類型等信息,第三個(gè)參數(shù)表示了第二個(gè)參數(shù)內(nèi)容的大小也可以使用sizeof(struct sockaddr)獲得。函數(shù)返回值表示其成功與否,同理,-1表示失敗,errno依然可以獲得其失敗的原因。

  • 寫入數(shù)據(jù)函數(shù)write()
    ?? 當(dāng)服務(wù)端在收到一個(gè)客戶端連接之后,可以通過套接字描述符進(jìn)行數(shù)據(jù)寫入工作,對(duì)套接字進(jìn)行寫入的形式和過程與普通文件的操作方式一致,內(nèi)核會(huì)根據(jù)文件描述符的值來查找所對(duì)應(yīng)的屬性,當(dāng)寫入對(duì)象為套接字的時(shí)候,會(huì)調(diào)用對(duì)應(yīng)的內(nèi)核函數(shù)。函數(shù)返回的大小為成功寫入的字節(jié)數(shù),寫入函數(shù)較為簡(jiǎn)單,這里不做更多介紹。
  • 讀取數(shù)據(jù)函數(shù)read()
    ?? 與寫入數(shù)據(jù)類似,使用read函數(shù)可以從套接字描述符中讀取數(shù)據(jù),當(dāng)然,在讀取數(shù)據(jù)之前,必須建立套接字連接,具體函數(shù)不做過多介紹。

總結(jié),本文僅僅介紹了本人在學(xué)習(xí)過程中的一些小知識(shí)以及TCP套接字建立簡(jiǎn)單的流程和其簡(jiǎn)單的用法,相信讀者在閱讀之后對(duì)TCP Socket的整個(gè)過程會(huì)有了一個(gè)比較好的把握(大神自動(dòng)忽略哈,嘻嘻)。涉及到TCP相關(guān)的其他異常控制,本文還沒對(duì)其進(jìn)行討論,最后附上完整的TCP服務(wù)端和客戶端代碼:https://github.com/OsCinux/TCP_Basic_Socket

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 簡(jiǎn)介 Socket理論 Socket工作流程 核心函數(shù)講解 服務(wù)的如何獲取客戶端的信息 字符串ip和網(wǎng)絡(luò)二進(jìn)制的轉(zhuǎn)...
    第八區(qū)閱讀 3,573評(píng)論 0 4
  • 一、基本socket函數(shù)Linux系統(tǒng)是通過提供套接字(socket)來進(jìn)行網(wǎng)絡(luò)編程的。網(wǎng)絡(luò)的socket數(shù)據(jù)傳輸...
    WB莫遙燚閱讀 1,497評(píng)論 0 0
  • 1三個(gè)相關(guān)數(shù)據(jù)結(jié)構(gòu). 關(guān)于socket的創(chuàng)建,首先需要分析socket這個(gè)結(jié)構(gòu)體,這是整個(gè)的核心。 104 str...
    ice_camel閱讀 2,880評(píng)論 1 8
  • socket通信原理 socket又被叫做套接字,它就像連接到兩端的插座孔一樣,通過建立管道,將兩個(gè)不同的進(jìn)程之間...
    jiodg45閱讀 1,184評(píng)論 0 1
  • 和堂妹有段時(shí)間沒見面,她畢業(yè)了一年比以前忙碌了很多,經(jīng)常看到朋友圈里她加班出差的狀態(tài)。前兩天空了見一面,她憔悴了很...
    如晚閱讀 535評(píng)論 2 0