Linux之本地Socket通信


一、Sokcet

學習路徑1:http://blog.csdn.net/u010073981/article/details/50734484
學習路徑2:https://www.cnblogs.com/cy568searchx/p/4211124.html
學習路徑3:https://www.cnblogs.com/yusenwu/p/4579167.html
為了防止資源丟失,整合如下:

socket起源於Unix,而Unix/Linux基本哲學之一就是“一切皆文件”,都可以用“打開open –> 讀寫write/read –> 關閉close”模式來操作。Socket就是該模式的一個實現, socket即是一種特殊的文件,一些socket函數就是對其進行的操作(讀/寫IO、打開、關閉).
說白了Socket是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在Socket接口后面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數據,以符合指定的協議。
注意:其實socket也沒有層的概念,它只是一個facade設計模式的應用,讓編程變的更簡單。是一個軟件抽象層。在網絡編程中,我們大量用的都是通過socket實現的。
使用套接字除了可以實現網絡間不同主機間的通信外,還可以實現同一主機的不同進程間的通信,且建立的通信是雙向的通信。socket進程通信與網絡通信使用的是統一套接口,只是地址結構與某些參數不同。

socket一詞的起源
在組網領域的首次使用是在1970年2月12日發布的文獻IETF RFC33中 發現的,撰寫者為Stephen Carr、Steve Crocker和Vint Cerf。根據美國計算機歷史博物館的記載,Croker寫道:“命名空間的元素都可稱為套接字接口。一個套接字接口構成一個連接的一端,而一個連接可完 全由一對套接字接口規定。”計算機歷史博物館補充道:“這比BSD的套接字接口定義早了大約12年。”

其主要流程如下:
這里寫圖片描述

二、Sokcet API

學習路徑1:http://blog.csdn.net/rangf/article/details/8350113
學習路徑2:http://blog.csdn.net/gladyoucame/article/details/8768731
為防止資源丟失,整合如下:

一、sockaddr 結構: struct sockaddr是通用的套接字地址 是linux 網絡通信的地址結構體的一種,此數據結構用做bind、connect、recvfrom、sendto等函數的參數,指明地址信息。

定義如下:

struct sockaddr { unsigned short sa_family ; /* address family*/ Char sa_data[14] ; /*up to 14 bytes of direct address */ } ;

 

頭文件: Sys/socket.h

說明:
Sa_family : 是地址家族,也成作,協議族,一般都是”AF_XXX”的形式,常用的有

AF_INET  Arpa(TCP/IP) 網絡通信協議(IPV4)
AF_INET6  IPV6
AF_UNIX  UNIX 域協議(文件系統套接字)(或稱AF_LOCAL   ,Unix域socket) AF_ISO ISO標准協議 AF_NS 施樂網絡體統協議 AF_IPX Novell IPX 協議 AF_APPLETALK Appletalk DDS AF_ROUTE 路由套接字 AF_KEY 密鑰套接字

 

Sa_data: 是14字節的協議地址

二、struct socketaddr_in : struct sockaddr是通用的套接字地址,而struct sockaddr_in則是internet環境下套接字的地址形式,二者長度一樣,都是16個字節。二者是並列結構,指向sockaddr_in結構的指針也可以指向sockaddr。一般情況下,需要把sockaddr_in結構強制轉換成sockaddr結構再傳入系統調用函數中。

定義如下:
IPV4:

struct in_addr { in_addr_t s_addr; }; struct sockaddr_in { uint8_t sin_len; //無符號8位整型 sa_family_t sin_famliy; /*AF_INET*/ in_port_t sin_port; struct in_addr sin_addr; /*32位 IPv4 地址*/ char sin_zero[8]; /*unuse*/ };

 

IPV6:

struct in6_addr { unsigned char s6_addr[16]; /* IPv6 address */ }; #define SIN6_LEN struct sockaddr_in6 { uint8_t sin6_len; sa_family_t sin6_famliy; /* 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 sockaddr_in { short int sin_family; /* Address family */ unsigned short int sin_port; /* Port number */ struct in_addr sin_addr; /* Internet address */ unsigned char sin_zero[8]; /* Same size as struct sockaddr */ }; struct in_addr { unsigned long s_addr; }; typedef struct in_addr { union { struct{ unsigned char s_b1, s_b2, s_b3, s_b4; } S_un_b; struct { unsigned short s_w1, s_w2; } S_un_w; unsigned long S_addr; } S_un; } IN_ADDR; 

 

頭文件:
sys/types.h
sa_family_t和socklen_t 頭文件 sys/socket.h
in_addr_t、 in_port_t 頭文件 netinet/in.h

說明:
sin_family 指代協議族,在socket編程中只能是AF_INET
sin_port 存儲端口號(使用網絡字節順序)
sin_addr 存儲IP地址,使用in_addr這個數據結構
sin_zero 是為了讓sockaddr與sockaddr_in兩個數據結構保持大小相同而保留的空字節。
s_addr 按照網絡字節順序存儲IP地址

三、Struct socketaddr_un : 針對UNIX域套接字地址, struct sockaddr是通用的套接字地址,而struct sockaddr_un則是UNIX環境下套接字的地址形式,人們在使用這種方式時往往用的不是網絡套接字,而是一種稱為本地套接字的方式。這樣做可以避免為黑客留下后門。一般情況下,需要把sockaddr_un結構強制轉換成sockaddr結構再傳入系統調用函數中。

定義如下:

Unix域對應的是: 
 #define UNIX_PATH_MAX 108 struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };

頭文件: sys/un.h :

說明:
sun_family 指代協議族,在socket編程中只能是AF_UNIX
sun_path 本地通信的路徑

四、socket()函數
sys/socket.h
int socket(int domain, int type, int protocol);

socket函數對應於普通文件的打開操作。普通文件的打開操作返回一個文件描述字,而socket()用於創建一個socket描述符(socket descriptor),它唯一標識一個socket。這個socket描述字跟文件描述字一樣,后續的操作都有用到它,把它作為參數,通過它來進行一些讀寫操作。

正如可以給fopen的傳入不同參數值,以打開不同的文件。創建socket的時候,也可以指定不同的參數創建不同的socket描述符,socket函數的三個參數分別為:

  1. domain:即協議域,又稱為協議族(family)。常用的協議族有,AF_INET、AF_INET6、AF_LOCAL(或稱AF_UNIX,Unix域socket)、AF_ROUTE等等。協議族決定了socket的地址類型,在通信中必須采用對應的地址,如AF_INET決定了要用ipv4地址(32位的)與端口號(16位的)的組合、AF_UNIX決定了要用一個絕對路徑名作為地址。
  2. type:指定socket類型。常用的socket類型有:

SOCK_STREAM(常用)字節流套接字
SOCK_DGRAM 數據報套接字
SOCK_SEQPACKET 有序分組套接字
SOCK_RAW 原始套接字

3 protocol:故名思意,就是指定協議。常用的協議有,

IPPROTO_TCP TCP傳輸協議
IPPTOTO_UDP UDP傳輸協議
IPPROTO_SCTP STCP傳輸協議
IPPROTO_TIPC TIPC傳輸協議

4 返回值:socket描述符

注意:並不是上面的type和protocol可以隨意組合的,如SOCK_STREAM不可以跟IPPROTO_UDP組合。當protocol為0時,會自動選擇type類型對應的默認協議。

當我們調用socket創建一個socket時,返回的socket描述字它存在於協議族(address family,AF_XXX)空間中,但沒有一個具體的地址。如果想要給它賦值一個地址,就必須調用bind()函數,否則就當調用connect()、listen()時系統會自動隨機分配一個端口。

五、bind()函數
sys/socket.h
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

正如上面所說bind()函數把一個地址族中的特定地址賦給socket。例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6地址和端口號組合賦給socket。

參數說明:

  • sockfd:即socket描述符,它是通過socket()函數創建了,唯一標識一個socket。bind()函數就是將給這個描述字綁定一個名字。
  • addr:一個const struct sockaddr*指針,指向要綁定給sockfd的協議地址。這個地址結構根據地址創建socket時的地址協議族的不同而不同
  • addrlen:對應的是地址的長度。
  • 返回值:成功標志

通常服務器在啟動的時候都會綁定一個眾所周知的地址(如ip地址+端口號),用於提供服務,客戶就可以通過它來接連服務器;而客戶端就不用指定,有系統自動分配一個端口號和自身的ip地址組合。這就是為什么通常服務器端在listen之前會調用bind(),而客戶端就不會調用,而是在connect()時由系統隨機生成一個。

六、listen()函數
sys/socket.h
int listen(int sockfd, int backlog);

服務器端套接字創建完畢並賦予本地地址值后,需要進行監聽,等待客戶端連接並處理請求,監聽使用 listen 系統調用,接受客戶端連接使用系統的accept()函數調用

參數說明:

  • sockfd:第一個參數即為要監聽的socket描述符
  • backlog:第二個參數為相應socket可以排隊的最大連接個數,表示排隊連接隊列的長度(若有多個客戶端同時連接,則需要進行排隊);
  • 返回值:成功標志

    socket()函數創建的socket默認是一個主動類型的,listen函數將socket變為被動類型的,等待客戶的連接請求。

七、connect()函數
sys/socket.h
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

客戶端通過調用connect函數來建立與TCP服務器的連接。

參數說明:

  • sockfd:第一個參數即為客戶端的socket描述字
  • addr:當前客戶端的本地地址,是一個 struct sockaddr_un 類型的變量,在不同主機中是struct sockaddr_in 類型的變量,
  • addrlen:表示本地地址的字節長度
  • 返回值:成功標志

八、accept()函數
sys/socket.h
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

TCP服務器端依次調用socket()、bind()、listen()之后,就會監聽指定的socket地址了。TCP客戶端依次調用socket()、connect()之后就想TCP服務器發送了一個連接請求。TCP服務器監聽到這個請求之后,就會調用accept()函數取接收請求,這樣連接就建立好了。之后就可以開始網絡I/O操作了,即類同於普通文件的讀寫I/O操作。

參數說明:

  • sockfd:第一個參數為服務器的socket描述符
  • addr:,第二個參數為指向struct sockaddr *的指針,用於返回客戶端的協議地址
  • addrlen:第三個參數為協議地址的長度
  • 返回值:如果accpet成功,那么其返回值是由內核自動生成的一個全新的描述字,代表與返回客戶的TCP連接。

注意:accept的第一個參數為服務器的socket描述字,是服務器開始調用socket()函數生成的,稱為監聽socket描述字;而accept函數返回的是已連接的socket描述字。一個服務器通常通常僅僅只創建一個監聽socket描述字,它在該服務器的生命周期內一直存在。內核為每個由服務器進程接受的客戶連接創建了一個已連接socket描述字,當服務器完成了對某個客戶的服務,相應的已連接socket描述字就被關閉。

九 、read()、write()函數
sys/socket.h
int read(int socket, char *buffer, size_t len);
int write(int socket, char *buffer, size_t len);

當然還有如下幾種格式的讀取和寫入:

#include <unistd.h> #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);

無論客戶端還是服務器,都要和對方進行數據上的交互。一個進程扮演客戶端的角色,另外一個進程扮演服務器的角色,兩個進程之間相互發送接收數據,這就是基於本地套接字的進程通信。
循環讀取客戶端發送的消息,當客戶端沒有發送數據時會阻塞直到有數據到來。如果想要多個連接並發處理,需要創建線程,將每個連接交給相應的線程並發處理。接收到數據后,進行相應的處理,將結果返回給客戶端。

參數說明:

  • socket:如果是服務端則是accpet()函數的返回值,客戶端是connect()函數中的第一個參數
  • buffer:寫入或者讀取的數據
  • len:寫入或者讀取的數據的大小

 

read函數是負責從fd中讀取內容.當讀成功時,read返回實際所讀的字節數,如果返回的值是0表示已經讀到文件的結束了,小於0表示出現了錯誤。如果錯誤為EINTR說明讀是由中斷引起的,如果是ECONNREST表示網絡連接出了問題。
write函數將buf中的nbytes字節內容寫入文件描述符fd.成功時返回寫的字節數。失敗時返回-1,並設置errno變量。 在網絡程序中,當我們向套接字文件描述符寫時有倆種可能。1)write的返回值大於0,表示寫了部分或者是全部的數據。2)返回的值小於0,此時出現了 錯誤。我們要根據錯誤類型來處理。如果錯誤為EINTR表示在寫的時候出現了中斷錯誤。如果為EPIPE表示網絡連接出現了問題(對方已經關閉了連接)。

十、close()函數
unistd.h
int close(int fd);

在服務器與客戶端建立連接之后,會進行一些讀寫操作,完成了讀寫操作就要關閉相應的socket描述字,好比操作完打開的文件要調用fclose關閉打開的文件。

參數說明:

  • fd:客戶端connect()函數的第一個參數,服務端accept()的返回值
  • 返回值:成功標志

close一個TCP socket的缺省行為時把該socket標記為以關閉,然后立即返回到調用進程。該描述字不能再由調用進程使用,也就是說不能再作為read或write的第一個參數。

注意:close操作只是使相應socket描述字的引用計數-1,只有當引用計數為0的時候,才會觸發TCP客戶端向服務器發送終止連接請求。

三、socket中TCP的三次握手建立連接

我們知道tcp建立連接要進行“三次握手”,即交換三個分組。大致流程如下:

  • 客戶端向服務器發送一個SYN J
  • 服務器向客戶端響應一個SYN K,並對SYN J進行確認ACK J+1
  • 客戶端再想服務器發一個確認ACK K+1

只有就完了三次握手,但是這個三次握手發生在socket的那幾個函數中呢?請看下圖:
這里寫圖片描述

從圖中可以看出,當客戶端調用connect時,觸發了連接請求,向服務器發送了SYN J包,這時connect進入阻塞狀態;服務器監聽到連接請求,即收到SYN J包,調用accept函 數接收請求向客戶端發送SYN K ,ACK J+1,這時accept進入阻塞狀態;客戶端收到服務器的SYN K ,ACK J+1之后,這時connect返回,並對SYN K進行確認;服務器收到ACK K+1時,accept返回,至此三次握手完畢,連接建立。

四、socket中TCP的四次握手釋放連接

圖解:
這里寫圖片描述

大致流程:

  • 某個應用進程首先調用close主動關閉連接,這時TCP發送一個FIN M;
  • 另一端接收到FIN M之后,執行被動關閉,對這個FIN進行確認。它的接收也作為文件結束符傳遞給應用進程,因為FIN的接收意味着應用進程在相應的連接上再也接收不到額外數據
  • 段時間之后,接收到文件結束符的應用進程調用close關閉它的socket。這導致它的TCP也發送一個FIN N;
  • 接收到這個FIN的源發送端TCP對它進行確認。
    這樣每個方向上都有一個FIN和ACK。

五、Example:

Client:
ClientSocket.h

#ifndef CLIENT_SOCKET_H #define CLIENT_SOCKET_H #include <sys/socket.h> #include <sys/un.h> #include <string.h> #include <stdint.h> #define SOCKET_PATH "clientSocket@com" class ClientSocket{ public: ClientSocket(); virtual ~ClientSocket(); int requestConnectService(); int read(); int write(); private: int fd; }; #endifClientSokcet.cpp
#include "ClientSocket.h" ClientSocket::ClientSocket():fd(-1){ } virtual ClientSocket::~ClientSocket(){ } int ClientSocket::requestConnectService(){ int result=-1; struct sockaddr_un server_sockaddr; fd= socket(AF_UNIX,SOCK_SEQPACKET,0); if(fd < 0) { return -1; } server_sockaddr.sun_family=AF_UNIX; server_sockaddr.sun_path[0] = 0; strcpy(server_sockaddr.sun_path+1, SOCKET_PATH); socklen_t len_t=sizeof(server_sockaddr.sun_family) + sizeof(BLUETOOTH_LE_SCOKET_PATH); result=connect(fd,( struct sockaddr * )&server_sockaddr,len_t); if(result<0){ close(fd); return -1; } return fd; } int ClientSocket::read(char* buf,unsigned int size){ int ret = -1; ret = write(fd,buf,size); if(ret<0){ return -1; } return ret; } int ClientSocket::write(char* buf,unsigned int size){ int ret = -1; ret = write(fd,buf,size); if(ret<0){ return -1; } return ret; }

Service:
ServicSokcet.h

#ifndef SERVICE_SOCKET_H #define SERVICE_SOCKET_H #include <sys/socket.h> #include <sys/un.h> #include <string.h> #include <stdint.h> #define SOCKET_PATH "clientSocket@com" #define LISTEN_MAX_SOCKET_NUM 20 class ServiceSocket{ public: ServiceSocket(); virtual ServiceSocket(); int createService(); int read(); int write(); private: int client_socket; }; #endif

ServiceSocket.cpp

#include "ServiceSocket.h" ServiceSocket::ServiceSocket():client_socket(-1){ } virtual ServiceSocket::ServiceSocket(){ } int ServiceSocket::createService(){ int serverFd = -1; struct sockaddr_un serv_addr; struct sockaddr_un client_addr; int client_addr_len = sizeof(struct sockaddr_un); memset(&client_addr,0x00,sizeof(client_addr)); char cmd[100] = "\0"; serverFd = socket(AF_UNIX,SOCK_SEQPACKET,0); if(serverFd < 0) { return -1; } memset(&serv_addr,0x00,sizeof(serv_addr)); serv_addr.sun_family = AF_UNIX; serv_addr.sun_path[0] = 0; strcpy(serv_addr.sun_path+1,SOCKET_PATH); socklen_t addrlen = sizeof(serv_addr.sun_family) + sizeof(SOCKET_PATH); if(bind(serverFd,(struct sockaddr*)&serv_addr,addrlen) < 0) { close(serverFd); return -1; } if(listen(serverFd,LISTEN_MAX_SOCKET_NUM) < 0) { close(serverFd); return -1; } int flags = 0; //獲取文件的flags fcntl(serverFd, F_GETFL, &flags); //增加文件的某個flags,比如文件是阻塞的,想設置成非阻塞: fcntl(serverFd, F_SETFL, O_NONBLOCK | flags); client_socket = accept(serverFd,(struct sockaddr*)&client_addr,(socklen_t*)&client_addr_len); if(client_socket > 0){ int flags = 0; fcntl(client_socket, F_GETFL,flags); fcntl(client_socket, F_SETFL, O_NONBLOCK | flags); return client_socket; } return -1; } int ServiceSocket::read(char* buf,unsigned int size){ int ret = -1; ret = write(client_socket,buf,size); if(ret<0){ return -1; } return ret; } int ServiceSocket::write(char* buf,unsigned int size){ int ret = -1; ret = write(client_socket,buf,size); if(ret<0){ return -1; } return ret; } gitHub源碼下載 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM