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

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

Linux|Centos|Ubuntu|系統(tǒng)進程|Fedora|注冊表|Bios|Solaris|Windows7|Windows10|Windows11|windows server|

服務器之家 - 服務器系統(tǒng) - Linux - Linux Socket 編程簡介和實現(xiàn)

Linux Socket 編程簡介和實現(xiàn)

2022-03-01 17:51sparkdev Linux

這篇文章主要介紹了Linux Socket 編程簡介和實現(xiàn),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧

在 tcp/ip 協(xié)議中,"ip地址 + tcp或udp端口號" 可以唯一標識網(wǎng)絡通訊中的一個進程,"ip地址+端口號" 就稱為 socket。本文以一個簡單的 tcp 協(xié)議為例,介紹如何創(chuàng)建基于 tcp 協(xié)議的網(wǎng)絡程序。

tcp 協(xié)議通訊流程

下圖描述了 tcp 協(xié)議的通訊流程(此圖來自互聯(lián)網(wǎng)):

Linux Socket 編程簡介和實現(xiàn)

下圖則描述 tcp 建立連接的過程(此圖來自互聯(lián)網(wǎng)):

Linux Socket 編程簡介和實現(xiàn)

服務器調用 socket()、bind()、listen() 函數(shù)完成初始化后,調用 accept() 阻塞等待,處于監(jiān)聽端口的狀態(tài),客戶端調用 socket() 初始化后,調用 connect() 發(fā)出 syn 段并阻塞等待服務器應答,服務器應答一個syn-ack 段,客戶端收到后從 connect() 返回,同時應答一個 ack 段,服務器收到后從 accept() 返回。

tcp 連接建立后數(shù)據(jù)傳輸?shù)倪^程:

建立連接后,tcp 協(xié)議提供全雙工的通信服務,但是一般的客戶端/服務器程序的流程是由客戶端主動發(fā)起請求,服務器被動處理請求,一問一答的方式。因此,服務器從 accept() 返回后立刻調用 read(),讀 socket 就像讀管道一樣,如果沒有數(shù)據(jù)到達就阻塞等待,這時客戶端調用 write() 發(fā)送請求給服務器,服務器收到后從 read() 返回,對客戶端的請求進行處理,在此期間客戶端調用 read() 阻塞等待服務器的應答,服務器調用 write() 將處理結果發(fā)回給客戶端,再次調用 read() 阻塞等待下一條請求,客戶端收到后從 read() 返回,發(fā)送下一條請求,如此循環(huán)下去。

下圖描述了關閉 tcp 連接的過程:

Linux Socket 編程簡介和實現(xiàn)

如果客戶端沒有更多的請求了,就調用 close() 關閉連接,就像寫端關閉的管道一樣,服務器的 read() 返回 0,這樣服務器就知道客戶端關閉了連接,也調用 close() 關閉連接。注意,任何一方調用 close() 后,連接的兩個傳輸方向都關閉,不能再發(fā)送數(shù)據(jù)了。如果一方調用 shutdown() 則連接處于半關閉狀態(tài),仍可接收對方發(fā)來的數(shù)據(jù)。

在學習 socket 編程時要注意應用程序和 tcp 協(xié)議層是如何交互的:

  1. 應用程序調用某個 socket 函數(shù)時 tcp 協(xié)議層完成什么動作,比如調用 connect() 會發(fā)出 syn 段
  2. 應用程序如何知道 tcp 協(xié)議層的狀態(tài)變化,比如從某個阻塞的 socket 函數(shù)返回就表明 tcp 協(xié)議收到了某些段,再比如 read() 返回 0 就表明收到了 fin 段

下面通過一個簡單的 tcp 網(wǎng)絡程序來理解相關概念。程序分為服務器端和客戶端兩部分,它們之間通過 socket 進行通信。

服務器端程序

下面是一個非常簡單的服務器端程序,它從客戶端讀字符,然后將每個字符轉換為大寫并回送給客戶端:

?
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
52
53
54
55
56
57
58
59
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
 
#define maxline 80
#define serv_port 8000
 
int main(void)
{
  struct sockaddr_in servaddr, cliaddr;
  socklen_t cliaddr_len;
  int listenfd, connfd;
  char buf[maxline];
  char str[inet_addrstrlen];
  int i, n;
 
  // socket() 打開一個網(wǎng)絡通訊端口,如果成功的話,
  // 就像 open() 一樣返回一個文件描述符,
  // 應用程序可以像讀寫文件一樣用 read/write 在網(wǎng)絡上收發(fā)數(shù)據(jù)。
  listenfd = socket(af_inet, sock_stream, 0);
 
  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = af_inet;
  servaddr.sin_addr.s_addr = htonl(inaddr_any);
  servaddr.sin_port = htons(serv_port);
  
  // bind() 的作用是將參數(shù) listenfd 和 servaddr 綁定在一起,
  // 使 listenfd 這個用于網(wǎng)絡通訊的文件描述符監(jiān)聽 servaddr 所描述的地址和端口號。
  bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
 
  // listen() 聲明 listenfd 處于監(jiān)聽狀態(tài),
  // 并且最多允許有 20 個客戶端處于連接待狀態(tài),如果接收到更多的連接請求就忽略。
  listen(listenfd, 20);
 
  printf("accepting connections ...\n");
  while (1)
  {
    cliaddr_len = sizeof(cliaddr);
    // 典型的服務器程序可以同時服務于多個客戶端,
    // 當有客戶端發(fā)起連接時,服務器調用的 accept() 返回并接受這個連接,
    // 如果有大量的客戶端發(fā)起連接而服務器來不及處理,尚未 accept 的客戶端就處于連接等待狀態(tài)。
    connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
   
    n = read(connfd, buf, maxline);
    printf("received from %s at port %d\n",
        inet_ntop(af_inet, &cliaddr.sin_addr, str, sizeof(str)),
        ntohs(cliaddr.sin_port));
  
    for (i = 0; i < n; i++)
    {
      buf[i] = toupper(buf[i]);
    }
      
    write(connfd, buf, n);
    close(connfd);
  }
}

把上面的代碼保存到文件 server.c 文件中,并執(zhí)行下面的命令編譯:

?
1
$ gcc server.c -o server

然后運行編譯出來的 server 程序:

?
1
$ ./server

此時我們可以通過 ss 命令來查看主機上的端口監(jiān)聽情況:

Linux Socket 編程簡介和實現(xiàn)

如上圖所示,server 程序已經(jīng)開始監(jiān)聽主機的 8000 端口了。

下面讓我們介紹一下這段程序中用到的 socket 相關的 api。

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

socket() 打開一個網(wǎng)絡通訊端口,如果成功的話,就像 open() 一樣返回一個文件描述符,應用程序可以像讀寫文件一樣用 read/write 在網(wǎng)絡上收發(fā)數(shù)據(jù)。對于ipv4,family 參數(shù)指定為 af_inet。對于 tcp 協(xié)議,type 參數(shù)指定為 sock_stream,表示面向流的傳輸協(xié)議。如果是 udp 協(xié)議,則 type 參數(shù)指定為 sock_dgram,表示面向數(shù)據(jù)報的傳輸協(xié)議。protocol 指定為 0 即可。

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

服務器需要調用 bind 函數(shù)綁定一個固定的網(wǎng)絡地址和端口號。bind() 的作用是將參數(shù) sockfd 和 myaddr 綁定在一起,使 sockfd 這個用于網(wǎng)絡通訊的文件描述符監(jiān)聽 myaddr 所描述的地址和端口號。struct sockaddr *是一個通用指針類型,myaddr 參數(shù)實際上可以接受多種協(xié)議的 sockaddr 結構體,而它們的長度各不相同,所以需要第三個參數(shù) addrlen 指定結構體的長度。

程序中對 myaddr 參數(shù)的初始化為:

?
1
2
3
4
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = af_inet;
servaddr.sin_addr.s_addr = htonl(inaddr_any);
servaddr.sin_port = htons(serv_port);

首先將整個結構體清零,然后設置地址類型為 af_inet,網(wǎng)絡地址為 inaddr_any,這個宏表示本地的任意 ip 地址,因為服務器可能有多個網(wǎng)卡,每個網(wǎng)卡也可能綁定多個 ip 地址,這樣設置可以在所有的 ip 地址上監(jiān)聽,直到與某個客戶端建立了連接時才確定下來到底用哪個 ip 地址,端口號為 serv_port,我們定義為 8000。

?
1
int listen(int sockfd, int backlog);

listen() 聲明 sockfd 處于監(jiān)聽狀態(tài),并且最多允許有 backlog 個客戶端處于連接待狀態(tài),如果接收到更多的連接請求就忽略。

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

三方握手完成后,服務器調用 accept() 接受連接,如果服務器調用 accept() 時還沒有客戶端的連接請求,就阻塞等待直到有客戶端連接上來。cliaddr 是一個傳出參數(shù),accept() 返回時傳出客戶端的地址和端口號。addrlen 參數(shù)是一個傳入傳出參數(shù)(value-result argument),傳入的是調用者提供的緩沖區(qū) cliaddr 的長度以避免緩沖區(qū)溢出問題,傳出的是客戶端地址結構體的實際長度(有可能沒有占滿調用者提供的緩沖區(qū))。如果給 cliaddr 參數(shù)傳 null,表示不關心客戶端的地址。

服務器程序的主要結構如下:

?
1
2
3
4
5
6
7
8
9
while (1)
{
  cliaddr_len = sizeof(cliaddr);
  connfd = accept(listenfd,
      (struct sockaddr *)&cliaddr, &cliaddr_len);
  n = read(connfd, buf, maxline);
  ......
  close(connfd);
}

整個是一個 while 死循環(huán),每次循環(huán)處理一個客戶端連接。由于 cliaddr_len 是傳入傳出參數(shù),每次調用 accept( ) 之前應該重新賦初值。accept() 的參數(shù) listenfd 是先前的監(jiān)聽文件描述符,而 accept() 的返回值是另外一個文件描述符 connfd,之后與客戶端之間就通過這個 connfd 通訊,最后關閉 connfd 斷開連接,而不關閉 listenfd,再次回到循環(huán)開頭 listenfd 仍然用作 accept 的參數(shù)。

客戶端程序

下面是客戶端程序,它從命令行參數(shù)中獲得一個字符串發(fā)給服務器,然后接收服務器返回的字符串并打印:

?
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define maxline 80
#define serv_port 8000
 
int main(int argc, char *argv[])
{
  struct sockaddr_in servaddr;
  char buf[maxline];
  int sockfd, n;
  char *str;
  
  if (argc != 2)
  {
    fputs("usage: ./client message\n", stderr);
    exit(1);
  }
  str = argv[1];
  
  sockfd = socket(af_inet, sock_stream, 0);
 
  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = af_inet;
  inet_pton(af_inet, "127.0.0.1", &servaddr.sin_addr);
  servaddr.sin_port = htons(serv_port);
  
  // 由于客戶端不需要固定的端口號,因此不必調用 bind(),客戶端的端口號由內核自動分配。
  // 注意,客戶端不是不允許調用 bind(),只是沒有必要調用 bind() 固定一個端口號,
  // 服務器也不是必須調用 bind(),但如果服務器不調用 bind(),內核會自動給服務器分配監(jiān)聽端口,
  // 每次啟動服務器時端口號都不一樣,客戶端要連接服務器就會遇到麻煩。
  connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
 
  write(sockfd, str, strlen(str));
 
  n = read(sockfd, buf, maxline);
  printf("response from server:\n");
  write(stdout_fileno, buf, n);
  printf("\n");
  close(sockfd);
  return 0;
}

把上面的代碼保存到文件 client.c 文件中,并執(zhí)行下面的命令編譯:

?
1
$ gcc client.c -o client

然后運行編譯出來的 client 程序:

?
1
$ ./client hello

此時服務器端會收到請求并返回轉換為大寫的字符串,并輸出相應的信息:

Linux Socket 編程簡介和實現(xiàn)

而客戶端在發(fā)送請求后會收到轉換過的字符串:

Linux Socket 編程簡介和實現(xiàn)

在客戶端的代碼中有兩點需要注意:

1. 由于客戶端不需要固定的端口號,因此不必調用 bind(),客戶端的端口號由內核自動分配。
2. 客戶端需要調用 connect() 連接服務器,connect 和 bind 的參數(shù)形式一致,區(qū)別在于 bind 的參數(shù)是自己的地址,而 connect 的參數(shù)是對方的地址。

至此我們已經(jīng)使用 socket 技術完成了一個最簡單的客戶端服務器程序,雖然離實際應用還非常遙遠,但就學習而言已經(jīng)足夠了。

提升服務器端的響應能力

雖然我們的服務器程序可以響應客戶端的請求,但是這樣的效率太低了。一般情況下服務器程序需要能夠同時處理多個客戶端的請求。可以通過 fork 系統(tǒng)調用創(chuàng)建子進程來處理每個請求,下面是大體的實現(xiàn)思路:

?
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
listenfd = socket(...);
bind(listenfd, ...);
listen(listenfd, ...);
while (1)
{
  connfd = accept(listenfd, ...);
  n = fork();
  if (n == -1)
  {
    perror("call to fork");
    exit(1);
  }
  else if (n == 0)
  {
    // 在子進程中處理客戶端的請求。
    close(listenfd);
    while (1)
    {
      read(connfd, ...);
      ...
      write(connfd, ...);
    }
    close(connfd);
    exit(0);
  }
  else
  {
    close(connfd);
  
}

此時父進程的任務就是不斷的創(chuàng)建子進程,而由子進程去響應客戶端的具體請求。通過這種方式,可以極大的提升服務器端的響應能力。

總結

本文通過一個簡單的建基于 tcp 協(xié)議的網(wǎng)絡程序介紹了 linux socket 編程中的基本概念。通過它我們可以了解到 socket 程序工作的基本原理,以及一些解決性能問題的思路。

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

原文鏈接:http://www.cnblogs.com/sparkdev/p/8341134.html

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 免费一区二区三区 | 精品国产一区二区三区天美传媒 | 九九热视频免费观看 | 精品国产一区在线观看 | www.理论片 | 成人毛片视频免费 | 护士hd欧美free性xxxx | 成人免费网站在线观看视频 | 激情五月少妇a | 精品一区二区电影 | 91久久91久久精品免观看 | 久久福利剧场 | 国产精品视频免费网站 | 黄色片网站免费观看 | 91午夜免费视频 | 欧美特一级片 | 成人不卡免费视频 | 精品爱爱 | 久久免费视频8 | 久草在线资源福利站 | 黄网站色成年大片免费高 | 午夜视频亚洲 | 亚洲精品成人久久 | 日韩色电影 | 中午字幕无线码一区2020 | 99精品视频一区二区三区 | 欧美顶级毛片在线播放小说 | 黄色一级电影网 | 欧美一级高潮 | 国产在线一级视频 | 国产免费看| www.91sao| 91女上位 在线播放 bt 自拍 另类 综合 欧美 | 粉嫩蜜桃麻豆免费大片 | av电影免费在线 | 亚洲成人涩涩 | 欧美aaaaa一级毛片在线 | 国产一级一国产一级毛片 | 日本教室三级在线看 | 亚洲午夜不卡 | 91久久国产露脸精品国产 |