激情久久久_欧美视频区_成人av免费_不卡视频一二三区_欧美精品在欧美一区二区少妇_欧美一区二区三区的

服務器之家:專注于服務器技術及軟件下載分享
分類導航

Linux|Centos|Ubuntu|系統進程|Fedora|注冊表|Bios|Solaris|Windows7|Windows10|Windows11|windows server|

服務器之家 - 服務器系統 - Linux - 詳解Linux Socket編程(不限Linux)

詳解Linux Socket編程(不限Linux)

2021-12-07 16:58吳秦 Linux

本篇文章主要介紹了Linux Socket編程,網絡之間的通信全靠Socket,詳細的介紹了Socket,有興趣的同學可以了解一下。

我們深諳信息交流的價值,那網絡中進程之間如何通信,如我們每天打開瀏覽器瀏覽網頁時,瀏覽器的進程怎么與web服務器通信的?當你用qq聊天時,qq進程怎么與服務器或你好友所在的qq進程通信?這些都得靠socket?那什么是socket?socket的類型有哪些?還有socket的基本函數,這些都是本文想介紹的。本文的主要內容如下:

1、網絡中進程之間如何通信?

本地的進程間通信(ipc)有很多種方式,但可以總結為下面4類:

  • 消息傳遞(管道、fifo、消息隊列)
  • 同步(互斥量、條件變量、讀寫鎖、文件和寫記錄鎖、信號量)
  • 共享內存(匿名的和具名的)
  • 遠程過程調用(solaris門和sun rpc)

但這些都不是本文的主題!我們要討論的是網絡中進程之間如何通信?首要解決的問題是如何唯一標識一個進程,否則通信無從談起!在本地可以通過進程pid來唯一標識一個進程,但是在網絡中這是行不通的。其實tcp/ip協議族已經幫我們解決了這個問題,網絡層的“ip地址”可以唯一標識網絡中的主機,而傳輸層的“協議+端口”可以唯一標識主機中的應用程序(進程)。這樣利用三元組(ip地址,協議,端口)就可以標識網絡的進程了,網絡中的進程通信就可以利用這個標志與其它進程進行交互。

使用tcp/ip協議的應用程序通常采用應用編程接口:unix  bsd的套接字(socket)和unix system v的tli(已經被淘汰),來實現網絡進程之間的通信。就目前而言,幾乎所有的應用程序都是采用socket,而現在又是網絡時代,網絡中進程通信是無處不在,這就是我為什么說“一切皆socket”。

2、什么是socket?

上面我們已經知道網絡中的進程是通過socket來通信的,那什么是socket呢?socket起源于unix,而unix/linux基本哲學之一就是“一切皆文件”,都可以用“打開open –> 讀寫write/read –> 關閉close”模式來操作。我的理解就是socket就是該模式的一個實現,socket即是一種特殊的文件,一些socket函數就是對其進行的操作(讀/寫io、打開、關閉),這些函數我們在后面進行介紹。

socket一詞的起源

在組網領域的首次使用是在1970年2月12日發布的文獻ietf rfc33中發現的,撰寫者為stephen carr、steve crocker和vint cerf。根據美國計算機歷史博物館的記載,croker寫道:“命名空間的元素都可稱為套接字接口。一個套接字接口構成一個連接的一端,而一個連接可完全由一對套接字接口規定。”計算機歷史博物館補充道:“這比bsd的套接字接口定義早了大約12年。”

3、socket的基本操作

既然socket是“open—write/read—close”模式的一種實現,那么socket就提供了這些操作對應的函數接口。下面以tcp為例,介紹幾個基本的socket接口函數。

3.1、socket()函數

?
1
int socket(int domain, int type, int protocol);

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

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

  • domain:即協議域,又稱為協議族(family)。常用的協議族有,af_inet、af_inet6、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()時系統會自動隨機分配一個端口。

3.2、bind()函數

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

?
1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函數的三個參數分別為:

•sockfd:即socket描述字,它是通過socket()函數創建了,唯一標識一個socket。bind()函數就是將給這個描述字綁定一個名字。

•addr:一個const struct sockaddr *指針,指向要綁定給sockfd的協議地址。這個地址結構根據地址創建socket時的地址協議族的不同而不同,如ipv4對應的是:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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域對應的是:

?
1
2
3
4
5
6
#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。

3.3、listen()、connect()函數

如果作為一個服務器,在調用socket()、bind()之后就會調用listen()來監聽這個socket,如果客戶端這時調用connect()發出連接請求,服務器端就會接收到這個請求。

?
1
2
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服務器的連接。

3.4、accept()函數

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

?
1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

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

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

3.5、read()、write()等函數

萬事具備只欠東風,至此服務器與客戶已經建立好連接了。可以調用網絡i/o進行讀寫操作了,即實現了網咯中不同進程之間的通信!網絡i/o操作有下面幾組:

  • read()/write()
  • recv()/send()
  • readv()/writev()
  • recvmsg()/sendmsg()
  • recvfrom()/sendto()

我推薦使用recvmsg()/sendmsg()函數,這兩個函數是最通用的i/o函數,實際上可以把上面的其它函數都替換成這兩個函數。它們的聲明如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#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表示網絡連接出現了問題(對方已經關閉了連接)。

其它的我就不一一介紹這幾對i/o函數了,具體參見man文檔或者baidu、google,下面的例子中將使用到send/recv。

3.6、close()函數

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

?
1
2
#include <unistd.h>
int close(int fd);

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

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

4、socket中tcp的三次握手建立連接詳解

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

  • 客戶端向服務器發送一個syn j
  • 服務器向客戶端響應一個syn k,并對syn j進行確認ack j+1
  • 客戶端再想服務器發一個確認ack k+1

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

詳解Linux Socket編程(不限Linux)

圖1、socket中發送的tcp三次握手

從圖中可以看出,當客戶端調用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返回,至此三次握手完畢,連接建立。

總結:客戶端的connect在三次握手的第二個次返回,而服務器端的accept在三次握手的第三次返回。

5、socket中tcp的四次握手釋放連接詳解

上面介紹了socket中tcp的三次握手建立過程,及其涉及的socket函數。現在我們介紹socket中的四次握手釋放連接的過程,請看下圖:

詳解Linux Socket編程(不限Linux)

圖2、socket中發送的tcp四次握手

圖示過程如下:

  • 某個應用進程首先調用close主動關閉連接,這時tcp發送一個fin m;
  • 另一端接收到fin m之后,執行被動關閉,對這個fin進行確認。它的接收也作為文件結束符傳遞給應用進程,因為fin的接收意味著應用進程在相應的連接上再也接收不到額外數據;
  • 一段時間之后,接收到文件結束符的應用進程調用close關閉它的socket。這導致它的tcp也發送一個fin n;
  • 接收到這個fin的源發送端tcp對它進行確認。

這樣每個方向上都有一個fin和ack。

6、一個例子(實踐一下)

說了這么多了,動手實踐一下。下面編寫一個簡單的服務器、客戶端(使用tcp)——服務器端一直監聽本機的6666號端口,如果收到連接請求,將接收請求并接收客戶端發來的消息;客戶端與服務器端建立連接并發送一條消息。

服務器端代碼:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
 
#define maxline 4096
 
int main(int argc, char** argv)
{
  int  listenfd, connfd;
  struct sockaddr_in   servaddr;
  char  buff[4096];
  int   n;
 
  if( (listenfd = socket(af_inet, sock_stream, 0)) == -1 ){
  printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);
  exit(0);
  }
 
  memset(&servaddr, 0, sizeof(servaddr));
  servaddr.sin_family = af_inet;
  servaddr.sin_addr.s_addr = htonl(inaddr_any);
  servaddr.sin_port = htons(6666);
 
  if( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
  printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
  exit(0);
  }
 
  if( listen(listenfd, 10) == -1){
  printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
  exit(0);
  }
 
  printf("======waiting for client's request======\n");
  while(1){
  if( (connfd = accept(listenfd, (struct sockaddr*)null, null)) == -1){
    printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
    continue;
  }
  n = recv(connfd, buff, maxline, 0);
  buff[n] = '\0';
  printf("recv msg from client: %s\n", buff);
  close(connfd);
  }
 
  close(listenfd);
}

客戶端代碼:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
 
#define maxline 4096
 
int main(int argc, char** argv)
{
  int  sockfd, n;
  char  recvline[4096], sendline[4096];
  struct sockaddr_in  servaddr;
 
  if( argc != 2){
  printf("usage: ./client <ipaddress>\n");
  exit(0);
  }
 
  if( (sockfd = socket(af_inet, sock_stream, 0)) < 0){
  printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
  exit(0);
  }
 
  memset(&servaddr, 0, sizeof(servaddr));
  servaddr.sin_family = af_inet;
  servaddr.sin_port = htons(6666);
  if( inet_pton(af_inet, argv[1], &servaddr.sin_addr) <= 0){
  printf("inet_pton error for %s\n",argv[1]);
  exit(0);
  }
 
  if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
  printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
  exit(0);
  }
 
  printf("send msg to server: \n");
  fgets(sendline, 4096, stdin);
  if( send(sockfd, sendline, strlen(sendline), 0) < 0)
  {
  printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
  exit(0);
  }
 
  close(sockfd);
  exit(0);
}

當然上面的代碼很簡單,也有很多缺點,這就只是簡單的演示socket的基本函數使用。其實不管有多復雜的網絡程序,都使用的這些基本函數。上面的服務器使用的是迭代模式的,即只有處理完一個客戶端請求才會去處理下一個客戶端的請求,這樣的服務器處理能力是很弱的,現實中的服務器都需要有并發處理能力!為了需要并發處理,服務器需要fork()一個新的進程或者線程去處理請求等。

7、動動手

留下一個問題,歡迎大家回帖回答!!!是否熟悉linux下網絡編程?如熟悉,編寫如下程序完成如下功能:

服務器端:

接收地址192.168.100.2的客戶端信息,如信息為“client query”,則打印“receive query”

客戶端:

向地址192.168.100.168的服務器端順序發送信息“client query test”,“cleint query”,“client query quit”,然后退出。

題目中出現的ip地址可以根據實際情況定。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。

原文鏈接:http://www.cnblogs.com/skynet/archive/2010/12/12/1903949.html

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 香蕉成人在线观看 | 国产69精品福利视频 | 亚洲码无人客一区二区三区 | 日韩中文字幕一区二区三区 | 91九色视频 | 久久av喷吹av高潮av懂色 | 91毛片网站 | www.热 | 一级片久久免费 | 热久久91| 免费国产视频大全入口 | 国产91av视频 | 成人短视频在线观看 | 一级黄色播放 | 欧美一级特黄a | 深夜小视频在线观看 | 青草视频在线观看视频 | 精品欧美一区二区精品久久 | 成年人视频免费看 | 99亚洲视频 | 91av资源在线| 欧美性色大片 | 毛片在线免费视频 | 久久99精品久久久久久国产越南 | 永久在线观看电影 | 精品国产一区二区三区久久久蜜月 | 国产精品视频一区二区三区四区五区 | 婷婷久久综合九色综合色多多蜜臀 | 精品国产一区二区三区四区在线 | 9丨九色丨国产 | 在线a | 羞羞网站在线观看入口免费 | 欧产日产国产精品v | 国产日本在线 | 欧美成年私人网站 | 国产精品自拍片 | 精品亚洲午夜久久久久91 | va免费视频 | 欧美日韩专区国产精品 | 国产91影院 | 19禁国产精品福利视频 |