目錄html
Linux NIO 系列(02) 阻塞式 IOlinux
Netty 系列目錄(http://www.javashuo.com/article/p-hskusway-em.html)編程
yum install -y gcc nc man socket # 幫助手冊
關於 Linux 下 man 手冊的安裝和使用點擊這裏設計模式
(1) 編譯數組
[root@localhost test]# gcc socket.c -o socket
(2) 啓動服務端服務器
[root@localhost test]# ./socket
(3) nc模擬客務端網絡
[root@localhost ~]# nc 127.0.0.1 8888 hello,world
這裏服務端收到 hello,world 的請求,但同時打開多個 nc 時沒法響應。數據結構
[root@localhost test]# ./socket hello,world
Socket 起源於 Unix,而 Unix/Linux 基本哲學之一就是「一切皆文件」,均可以用 「打開open –> 讀寫write/read –> 關閉close」 模式來操做。Socket 就是該模式的一個實現,socket 便是一種特殊的文件,一些 socket 函數就是對其進行的操做(讀/寫IO、打開、關閉)。socket
說白了 Socket 是應用層與 TCP/IP 協議族通訊的中間軟件抽象層,它是一組接口。在設計模式中,Socket 其實就是一個門面模式,它把複雜的 TCP/IP 協議族隱藏在 Socket 接口後面,對用戶來講,一組簡單的接口就是所有,讓 Socket 去組織數據,以符合指定的協議。tcp
注意:其實 socket 也沒有層的概念,它只是一個 facade 設計模式的應用,讓編程變的更簡單。是一個軟件抽象層。在網絡編程中,咱們大量用的都是經過 socket 實現的。
其實就是一個整數,咱們最熟悉的句柄是 0、一、2 三個,0 是標準輸入,1 是標準輸出,2 是標準錯誤輸出。0、一、2 是整數表示的,對應的 FILE * 結構的表示就是 stdin、stdout、stderr
套接字 API 最初是做爲 UNIX 操做系統的一部分而開發的,因此套接字 API 與系統的其餘 I/O 設備集成在一塊兒。特別是,當應用程序要爲因特網通訊而建立一個套接字(socket)時,操做系統就返回一個小整數做爲描述符(descriptor)來標識這個套接字。而後,應用程序以該描述符做爲傳遞參數,經過調用函數來完成某種操做(例如經過網絡傳送數據或接收輸入的數據)。
在許多操做系統中,套接字描述符和其餘 I/O 描述符是集成在一塊兒的,因此應用程序能夠對文件進行套接字 I/O 或 I/O 讀/寫操做。
當應用程序要建立一個套接字時,操做系統就返回一個小整數做爲描述符,應用程序則使用這個描述符來引用該套接字須要 I/O 請求的應用程序請求操做系統打開一個文件。操做系統就建立一個文件描述符提供給應用程序訪問文件。從應用程序的角度看,文件描述符是一個整數,應用程序能夠用它來讀寫文件。下圖顯示,操做系統如何把文件描述符實現爲一個指針數組,這些指針指向內部數據結構。
對於每一個程序系統都有一張單獨的表。精確地講,系統爲每一個運行的進程維護一張單獨的文件描述符表。當進程打開一個文件時,系統把一個指向此文件內部數據結構的指針寫入文件描述符表,並把該表的索引值返回給調用者 。應用程序只需記住這個描述符,並在之後操做該文件時使用它。操做系統把該描述符做爲索引訪問進程描述符表,經過指針找到保存該文件全部的信息的數據結構。
針對套接字的系統數據結構:
1)、套接字 API 裏有個函數 socket,它就是用來建立一個套接字。套接字設計的整體思路是,單個系統調用就能夠建立任何套接字,由於套接字是至關籠統的。一旦套接字建立後,應用程序還須要調用其餘函數來指定具體細節。例如調用 socket 將建立一個新的描述符條目:
2)、雖然套接字的內部數據結構包含不少字段,可是系統建立套接字後,大多數字字段沒有填寫。應用程序建立套接字後在該套接字可使用以前,必須調用其餘的過程來填充這些字段。
文件描述符:在 linux 系統中打開文件就會得到文件描述符,它是個很小的正整數。每一個進程在 PCB(Process Control Block)中保存着一份文件描述符表,文件描述符就是這個表的索引,每一個表項都有一個指向已打開文件的指針。
文件指針:C 語言中使用文件指針作爲 I/O 的句柄。文件指針指向進程用戶區中的一個被稱爲 FILE 結構的數據結構。FILE 結構包括一個緩衝區和一個文件描述符。而文件描述符是文件描述符表的一個索引,所以從某種意義上說文件指針就是句柄的句柄(在 Windows 系統上,文件描述符被稱做文件句柄)。
在生活中,A 要電話給 B,A 撥號,B 聽到電話鈴聲後提起電話,這時 A 和 B 就創建起了鏈接,A 和 B 就能夠講話了。等交流結束,掛斷電話結束這次交談。 打電話很簡單解釋了這工做原理:「open—write/read—close」模式。
服務器端先初始化 Socket,而後與端口綁定(bind),對端口進行監聽(listen),調用 accept 阻塞,等待客戶端鏈接。在這時若是有個客戶端初始化一個 Socket,而後鏈接服務器(connect),若是鏈接成功,這時客戶端與服務器端的鏈接就創建了。客戶端發送數據請求,服務器端接收請求並處理請求,而後把迴應數據發送給客戶端,客戶端讀取數據,最後關閉鏈接,一次交互結束。
這些接口的實現都是內核來完成。具體如何實現,能夠看看 linux 的內核
int socket(int protofamily, int type, int protocol); //返回 sockfd(文件描述符)
socket 函數對應於普通文件的打開操做。普通文件的打開操做返回一個文件描述字,而 socket() 用於建立一個 socket 描述符(socket descriptor),它惟一標識一個 socket。這個 socket 描述字跟文件描述字同樣,後續的操做都有用到它,把它做爲參數,經過它來進行一些讀寫操做。
正如能夠給 fopen 的傳入不一樣參數值,以打開不一樣的文件。建立 socket 的時候,也能夠指定不一樣的參數建立不一樣的 socket 描述符,socket 函數的三個參數分別爲:
protofamily:即協議域,又稱爲協議族(family)。經常使用的協議族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或稱 AF_UNIX,Unix 域 socket)、AF_ROUTE 等等。協議族決定了 socket 的地址類型,在通訊中必須採用對應的地址,如 AF_INET 決定了要用 ipv4 地址(32位的)與端口號(16位的)的組合、AF_UNIX 決定了要用一個絕對路徑名做爲地址。
type:指定 socket 類型。經常使用的 socket 類型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET 等等(socket的類型有哪些?)。
protocol:故名思意,就是指定協議。經常使用的協議有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC 等,它們分別對應 TCP 傳輸協議、UDP 傳輸協議、STCP 傳輸協議、TIPC 傳輸協議。
注意: 並非上面的 type 和 protocol 能夠隨意組合的,如 SOCK_STREAM 不能夠跟 IPPROTO_UDP 組合。當 protocol 爲 0 時,會自動選擇 type 類型對應的默認協議。
當咱們調用 socket 建立一個 socket 時,返回的 socket 描述字它存在於協議族(address family,AF_XXX)空間中,但沒有一個具體的地址。若是想要給它賦值一個地址,就必須調用 bind() 函數,不然就當調用 connect()、listen() 時系統會自動隨機分配一個端口。
# tcp/ipv4 int listenfd = socket(AF_INET, SOCK_STREAM, 0);
正如上面所說 bind() 函數把一個地址族中的特定地址賦給 socket。例如對應 AF_INET、AF_INET6 就是把一個 ipv4 或 ipv6 地址和端口號組合賦給 socket。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函數的三個參數分別爲:
sockfd:即 socket 描述字,它是經過 socket() 函數建立了,惟一標識一個 socket。bind() 函數就是將給這個描述字綁定一個名字。
addr:一個 const struct sockaddr * 指針,指向要綁定給 sockfd 的協議地址。這個地址結構根據地址建立 socket 時的地址協議族的不一樣而不一樣,如 ipv4 對應的是:
struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */ }; /* Internet address. */ struct in_addr { uint32_t s_addr; /* address in network byte order */ }; # ipv6對應的是: struct sockaddr_in6 { sa_family_t sin6_family; /* AF_INET6 */ in_port_t sin6_port; /* port number */ uint32_t sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */ }; struct in6_addr { unsigned char s6_addr[16]; /* IPv6 address */ }; # Unix域對應的是: #define UNIX_PATH_MAX 108 struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };
addrlen:對應的是地址的長度。
一般服務器在啓動的時候都會綁定一個衆所周知的地址(如ip地址+端口號),用於提供服務,客戶就能夠經過它來接連服務器;而客戶端就不用指定,有系統自動分配一個端口號和自身的 ip 地址組合。這就是爲何一般服務器端在 listen 以前會調用 bind(),而客戶端就不會調用,而是在 connect() 時由系統隨機生成一個。
網絡字節序與主機字節序
主機字節序就是咱們日常說的大端和小端模式:不一樣的 CPU 有不一樣的字節序類型,這些字節序是指整數在內存中保存的順序,這個叫作主機序。引用標準的 Big-Endian 和 Little-Endian 的定義以下:
a) Little-Endian就是低位字節排放在內存的低地址端,高位字節排放在內存的高地址端。
b) Big-Endian就是高位字節排放在內存的低地址端,低位字節排放在內存的高地址端。
網絡字節序:4 個字節的 32 bit 值如下面的次序傳輸:首先是 0 ~ 7bit,其次 8 ~ 15bit,而後 16 ~ 23bit,最後是 24 ~ 31bit。這種傳輸次序稱做大端字節序。因爲 TCP/IP 首部中全部的二進制整數在網絡中傳輸時都要求以這種次序,所以它又稱做網絡字節序。字節序,顧名思義字節的順序,就是大於一個字節類型的數據在內存中的存放順序,一個字節的數據沒有順序的問題了。
因此:在將一個地址綁定到 socket 的時候,請先將主機字節序轉換成爲網絡字節序,而不要假定主機字節序跟網絡字節序同樣使用的是 Big-Endian。因爲這個問題曾引起過血案!公司項目代碼中因爲存在這個問題,致使了不少莫名其妙的問題,因此請謹記對主機字節序不要作任何假定,務必將其轉化爲網絡字節序再賦給 socket。
若是做爲一個服務器,在調用 socket()、bind() 以後就會調用 listen() 來監聽這個 socket,若是客戶端這時調用 connect() 發出鏈接請求,服務器端就會接收到這個請求。
int listen(int sockfd, int backlog); int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen 函數的第一個參數即爲要監聽的 socket 描述字,第二個參數爲相應 socket 能夠排隊的最大鏈接個數。socket() 函數建立的 socket 默認是一個主動類型的,listen 函數將 socket 變爲被動類型的,等待客戶的鏈接請求。
connect 函數的第一個參數即爲客戶端的 socket 描述字,第二參數爲服務器的 socket 地址,第三個參數爲 socket 地址的長度。客戶端經過調用 connect 函數來創建與 TCP 服務器的鏈接。
TCP 服務器端依次調用 socket()、bind()、listen() 以後,就會監聽指定的 socket 地址了。TCP 客戶端依次調用 socket()、connect() 以後就向 TCP 服務器發送了一個鏈接請求。TCP 服務器監聽到這個請求以後,就會調用 accept() 函數取接收請求,這樣鏈接就創建好了。以後就能夠開始網絡 I/O 操做了,即類同於普通文件的讀寫 I/O 操做。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回鏈接 connect_fd
sockfd:
參數 sockfd 就是上面解釋中的監聽套接字,這個套接字用來監聽一個端口,當有一個客戶與服務器鏈接時,它使用這個一個端口號,而此時這個端口號正與這個套接字關聯。固然客戶不知道套接字這些細節,它只知道一個地址和一個端口號。
addr:
這是一個結果參數,它用來接受一個返回值,這返回值指定客戶端的地址,固然這個地址是經過某個地址結構來描述的,用戶應該知道這一個什麼樣的地址結構。若是對客戶的地址不感興趣,那麼能夠把這個值設置爲 NULL。
len:
如同你們所認爲的,它也是結果的參數,用來接受上述 addr 的結構的大小的,它指明 addr 結構所佔有的字節個數。一樣的,它也能夠被設置爲 NULL。
若是 accept 成功返回,則服務器與客戶已經正確創建鏈接了,此時服務器經過 accept 返回的套接字來完成與客戶的通訊。
注意: accept 默認會阻塞進程,直到有一個客戶鏈接創建後返回,它返回的是一個新可用的套接字,這個套接字是鏈接套接字。
此時咱們須要區分兩種套接字,
監聽套接字: 監聽套接字正如 accept 的參數 sockfd,它是監聽套接字,在調用 listen 函數以後,是服務器開始調用 socket() 函數生成的,稱爲監聽 socket 描述字(監聽套接字)
鏈接套接字:一個套接字會從主動鏈接的套接字變身爲一個監聽套接字;而 accept 函數返回的是已鏈接 socket 描述字(一個鏈接套接字),它表明着一個網絡已經存在的點點鏈接。
一個服務器一般一般僅僅只建立一個監聽 socket 描述字,它在該服務器的生命週期內一直存在。內核爲每一個由服務器進程接受的客戶鏈接建立了一個已鏈接 socket 描述字,當服務器完成了對某個客戶的服務,相應的已鏈接 socket 描述字就被關閉。
天然要問的是:爲何要有兩種套接字?緣由很簡單,若是使用一個描述字的話,那麼它的功能太多,使得使用很不直觀,同時在內核確實產生了一個這樣的新的描述字。
鏈接套接字 socketfd_new 並無佔用新的端口與客戶端通訊,依然使用的是與監聽套接字 socketfd 同樣的端口號
萬事具有隻欠東風,至此服務器與客戶已經創建好鏈接了。能夠調用網絡 I/O 進行讀寫操做了,即實現了網咯中不一樣進程之間的通訊!網絡 I/O 操做有下面幾組:
我推薦使用 recvmsg()/sendmsg() 函數,這兩個函數是最通用的 I/O 函數,實際上能夠把上面的其它函數都替換成這兩個函數。它們的聲明以下:
#include <unistd.h> ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count); #include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t len, int flags); ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags); ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
read 函數是負責從 fd 中讀取內容.當讀成功時,read 返回實際所讀的字節數,若是返回的值是 0 表示已經讀到文件的結束了,小於 0 表示出現了錯誤。若是錯誤爲 EINTR 說明讀是由中斷引發的,若是是 ECONNREST 表示網絡鏈接出了問題。
write 函數將 buf 中的 nbytes 字節內容寫入文件描述符 fd.成功時返回寫的字節數。失敗時返回 -1,並設置 errno 變量。 在網絡程序中,當咱們向套接字文件描述符寫時有倆種可能。1) write 的返回值大於 0,表示寫了部分或者是所有的數據。2) 返回的值小於 0,此時出現了錯誤。咱們要根據錯誤類型來處理。若是錯誤爲 EINTR 表示在寫的時候出現了中斷錯誤。若是爲 EPIPE 表示網絡鏈接出現了問題(對方已經關閉了鏈接)。
在服務器與客戶端創建鏈接以後,會進行一些讀寫操做,完成了讀寫操做就要關閉相應的 socket 描述字,比如操做完打開的文件要調用 fclose 關閉打開的文件。
#include <unistd.h> int close(int fd);
close 一個 TCP socket 的缺省行爲時把該 socket 標記爲以關閉,而後當即返回到調用進程。該描述字不能再由調用進程使用,也就是說不能再做爲 read 或 write 的第一個參數。
注意:close 操做只是使相應 socket 描述字的引用計數 -1,只有當引用計數爲 0 的時候,纔會觸發 TCP 客戶端向服務器發送終止鏈接請求。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define SERVER_PORT 8888 #define BACKLOG 10 #define BUF_SIZE 1024 void main() { //網絡地址結構 server struct sockaddr_in my_addr; my_addr.sin_family = AF_INET; my_addr.sin_port = htons(SERVER_PORT); my_addr.sin_addr.s_addr = htonl(INADDR_ANY); char recv_buf[BUF_SIZE] = ""; //客戶端 struct sockaddr_in client_addr; int listenfd = socket(AF_INET, SOCK_STREAM, 0); bind(listenfd, (struct sockaddr *)&my_addr, sizeof(my_addr)); listen (listenfd, BACKLOG); socklen_t cliaddr_len = sizeof(client_addr); int clientfd = accept(listenfd, (struct sockaddr*)&client_addr, &cliaddr_len); //char client_ip; //inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN); //printf("client ip=%s,port=%d\n", client_ip, ntohs(client_addr.sin_port)); while(1) { int k = read(clientfd, recv_buf, sizeof(recv_buf)); if(k > 0) { printf("%s\n", recv_buf); } } }
參考:
天天用心記錄一點點。內容也許不重要,但習慣很重要!