第十六章:網絡IPC 套接字


 

一、IP地址和端口

套接字接口可以用於計算機間通信。目前計算機間使用套接字通訊需要保證處於同一網段。

 

為了查看是否處於同一網段,我們可以使用IP地址判斷。

IP地址是計算機在網絡中的唯一標識。IP地址本質是個整數,它與網卡的物理地址(MAC地址)綁定。MAC地址在網卡出廠時都確保唯一,不需要我們關心。

IP地址有IPv4和IPv6之分,IPv4是32位整數,IPv6是128位整數。現在使用的一般是IPv4。

為了便於記憶,IPv4地址的每個字節轉換為一個整數(8位整數,0到255),各個整數之間使用“.”隔開,這種方式叫點分十進制。如192.168.7.5。

人類使用的是點分十進制,計算機底層存儲的是整數格式(十六進制)。兩種表示方法在編程時需要互相轉換

 

當我們上網時輸入網址如baidu.com,這個baidu.com域名是IP地址的助記符。域名需要轉換成IP地址才能找到計算機,這個工作由域名解析服務器完成。

 

在Linux中,我們可以使用ifconfig查看當前計算機的IP地址

上圖中IPv4為127.0.0.1的lo為本地回環,簡單點說就是用於本機通訊。

 

可以看到我的系統的IPv4地址為7.7.7.5,若有和我處於一個網絡電腦的IPv4地址為7.7.7.3。我們需要使用位與判斷這兩個IP地址是否處於同一網段。

 

首先把7.7.7.5位與Mask(255.0.0.0),得到7.0.0.0;

之后把7.7.7.3位與Mask(255.0.0.0),得到7.0.0.0。

可以發現結果一致,表示兩IP處於同一網段。

 

但是IP地址只能定位計算機,不能定位計算機中的進程。為解決此問題,系統提供了端口。

端口用於計算機對外管理進程。因此網絡編程需要提供IP地址和端口號。

端口的本質是一個unsigned short(0到65535),代表了計算機中的每一個進程。這些進程中,有些端口已經被占用,編程中需要使用未被占用的端口。

0 到 1023:系統預先占用部分,最好別用

1024 到 48000:可以使用,但有個別端口被其它程序占用

48000 到 65535:系統可能會隨時使用某一個,不穩定

 

 

二、字節順序

有了IP和端口后,傳輸的源頭和目的地就有了,此時還需要數據和數據格式。

在網絡中,數據格式並不是確定的,因此我們在發送或接受數據時,需要把本機格式轉換網絡格式或把網絡格式還原本機格式。

 

IP本機格式轉換網絡格式使用函數為inet_addr(),如:inet_addr("7.7.7.5");

IP網絡格式轉換本機格式使用函數為inet_ntoa(),如:inet_ntoa(sockaddr.sin_addr);

 

端口本機格式轉換網絡格式使用函數為htons(),如:htons(8888);

 

 

三、套接字的編程步驟

網絡編程需要考率兩個方面:服務器端和客戶端。

服務器端的編程步驟:

1. 調用socket()函數,創建一個socket描述符,它和fd使用方法一致

2. 配置struct sockaddr_in或struct sockaddr_un,准備進行數據交互

3. 調用bind()函數,綁定通信地址和socket描述符

4. 調用read()、write(),讀寫socket描述符

5. 調用close()函數,關閉socket描述符

 

客戶端的編程步驟:

除了服務器端步驟3的bind()換成connect()以外,其它步驟和服務器端一樣,並且connect()和bind()用法完全一樣。

 

套接字類函數和結構體定義如下:

1. 創建socket描述符

#include <sys/types.h>
#include <sys/socket.h>

/* 創建socket描述符 */
int socket(int domain, int type, int protocol);

函數參數以及返回值:

domain:選擇協議,有以下宏:

AF_UNIX/AF_LOCAL/AF_FILE:本地通信

AF_INET:網絡通信IPv4

AF_INET6:網絡通信IPv6

type:通信類型,有以下宏:

SOCK_STREAM:數據流,用於TCP(有數據回傳機制,可以判斷是否發送成功)

SOCK_DGRAM:數據報,用於UDP(沒有數據回傳機制)

返回值:成功返回socket描述符;出錯返回-1。


 

2. 配置struct sockaddr_in或struct sockaddr_un

其實通信使用的結構體是sockaddr。它是sockaddr_in和sockaddr_un的一般化,sockaddr_in和sockaddr_un做參數時必須轉化為sockaddr。

在代碼中,sockaddr_in負責網絡通信;sockaddr_un負責本地通信。

 

其中sockaddr_un結構體定義如下:

#include <sys/un.h>

struct sockaddr_un
{
    int sun_family;     // 用於指定協議,和socket()的第一個參數保持一致
    char sun_path[];    // 存socket文件名(做交互媒介)
    // 注意,數組不能直接用=賦值
};

/* 示例 */
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "a.sock"); // 系統會自動創建a.sock文件

sockaddr_in結構體定義如下:

#include <netinet/in.h>

struct sockaddr_in
{
    int sin_family;    // 用於指定協議,和socket()的第一個參數保持一致
    short sin_port;    // 端口號
    struct in_addr sin_addr;    // 存儲IP地址的結構
};

/* 示例 */
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = inet_addr("7.7.7.5");

 

3. 綁定socket描述符和struct sockaddr

#include <sys/types.h>
#include <sys/socket.h>

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

/* 示例 */
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));

 

 

四、TCP通信

由於TCP會進行回傳工作等,因此TCP在前三步的基礎上,需要監聽客戶端回應並等待客戶端連接。

服務器端的編程步驟:

1. 調用socket()函數,創建一個socket描述符,它和fd使用方法一致

2. 配置struct sockaddr_in或struct sockaddr_un,准備進行數據交互

3. 調用bind()函數,綁定通信地址和socket描述符

4. 調用listen()函數,監聽客戶端

5. 調用accept()函數,等待客戶端連接。accept()會返回客戶端的socket描述符,用於讀寫交互

6. 調用read()、write(),讀寫socket描述符

7. 調用close()函數,關閉socket描述符

 

客戶端的編程步驟:

與之前一致

 

listen()函數定義如下:

#include <sys/types.h>
#include <sys/socket.h>

int listen(int sockfd, int backlog);

/* 示例 */
listen(sockfd, 100);    // 監聽100個客戶端

 

accept()函數定義如下:

#include <sys/types.h>
#include <sys/socket.h>

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

/* 示例 */
/* 客戶端fd                 返回的客戶端addr */
clientfd = accept(sockfd, (struct sockaddr*)&client, &clientlen);

 

服務器示例代碼如下:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 #include <sys/socket.h>
 5 #include <netinet/in.h>
 6 #include <arpa/inet.h>
 7 #include <unistd.h>
 8 
 9 int main()
10 {
11     int id = socket(AF_INET, SOCK_STREAM, 0);
12     if (id == -1)
13         perror("socket"), exit(-1);
14     
15     struct sockaddr_in addr;
16     addr.sin_family = AF_INET;
17     addr.sin_port = htons(2222);
18     addr.sin_addr.s_addr = inet_addr("127.0.0.1");
19 
20     /* 防止SIGINT后一段時間端口占用 */
21     int reuse = 1;
22     setsockopt(id, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
23 
24     int res = bind(id, (struct sockaddr*)&addr, sizeof(addr));
25     if (res == -1)
26         perror("bind"), exit(-1);
27     printf("綁定完成!\n");
28 
29     listen(id, 10);
30 
31     int client;
32     struct sockaddr_in from;
33     socklen_t len = sizeof(from);
34     
35     client = accept(id, (struct sockaddr*)&from, &len);
36     if (client == -1)
37         perror("accept"), exit(-1);
38     printf("%s連接了\n", inet_ntoa(from.sin_addr));
39     
40     char buf[100];
41     read(client, buf, sizeof(buf));
42     printf("接收數據為:%s\n", buf);
43 
44     close(client);
45 
46     return 0;
47 }

客戶端示例代碼如下:

 1 #include <stdio.h>
 2 #include <sys/socket.h>
 3 #include <netinet/in.h>
 4 #include <stdlib.h>
 5 #include <string.h>
 6 #include <arpa/inet.h>
 7 #include <unistd.h>
 8 
 9 int main()
10 {
11     int fd = socket(AF_INET, SOCK_STREAM, 0);
12     if (fd == -1)
13         perror("socket"), exit(-1);
14 
15     struct sockaddr_in sock;
16     sock.sin_family = AF_INET;
17     sock.sin_port = htons(2222);
18     sock.sin_addr.s_addr = inet_addr("127.0.0.1");
19 
20     int res = connect(fd, (struct sockaddr *)&sock, sizeof(sock));
21     if (res == -1)
22         perror("connect"),exit(-1);
23     printf("連接成功\n");
24 
25     char buf[100];
26 
27     strcpy(buf, "Hello World");
28 
29     res = write(fd, buf, strlen(buf));
30     printf("發送成功\n");
31 
32     close(fd);
33 
34     return 0;
35 }

 

測試時首先啟動服務器程序,之后啟動客戶端。

 

 

五、UDP通信

服務器端的編程步驟:

1. 調用socket()函數,創建一個socket描述符,它和fd使用方法一致

2. 配置struct sockaddr_in或struct sockaddr_un,准備進行數據交互

3. 調用bind()函數,綁定通信地址和socket描述符

4. 調用read()或recvfrom()讀socket描述符,調用sendto()寫socket描述符

5. 調用close()函數,關閉socket描述符

 

客戶端的編程步驟:

不需要之前的connect()函數;調用recvfrom()、sendto(),讀寫socket描述符

 

read()和recvfrom()的區別在於:

read()只能讀數據,不能讀發送方的通信地址,而recvfrom()兩者皆可

 

recvfrom()和sendto()函數定義如下:

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

其中,

參數flags用於指定是否阻塞,如不需要阻塞等待可設置為MSG_DONTWAIT,阻塞等待可設置為0。

參數src_addr表示發送數據的地址;參數dest_addr表示接收數據的地址。

需要注意的是recvfrom()函數的最后一個參數采用的是指針傳遞,而sendto采用的值傳遞。

 

服務器示例代碼如下:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 #include <sys/socket.h>
 5 #include <netinet/in.h>
 6 #include <arpa/inet.h>
 7 #include <unistd.h>
 8 
 9 int main()
10 {
11     int id = socket(AF_INET, SOCK_DGRAM, 0);
12     if (id == -1)
13         perror("socket"), exit(-1);
14     
15     struct sockaddr_in addr;
16     addr.sin_family = AF_INET;
17     addr.sin_port = htons(2222);
18     addr.sin_addr.s_addr = inet_addr("127.0.0.1");
19 
20     /* 防止SIGINT后一段時間端口占用 */
21     int reuse = 1;
22     setsockopt(id, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
23 
24     int res = bind(id, (struct sockaddr*)&addr, sizeof(addr));
25     if (res == -1)
26         perror("bind"), exit(-1);
27     printf("綁定完成!\n");
28 
29     int client;
30     struct sockaddr_in from;
31     socklen_t len = sizeof(from);
32     
33     char buf[100];
34     recvfrom(id, buf, sizeof(buf), 0, (struct sockaddr*)&from, &len);
35     printf("%s連接了,接收數據為:%s\n", inet_ntoa(from.sin_addr), buf);
36 
37     close(client);
38 
39     return 0;
40 }

客戶端示例代碼如下:

 1 #include <stdio.h>
 2 #include <sys/socket.h>
 3 #include <netinet/in.h>
 4 #include <stdlib.h>
 5 #include <string.h>
 6 #include <arpa/inet.h>
 7 #include <unistd.h>
 8 
 9 int main()
10 {
11     int fd = socket(AF_INET, SOCK_DGRAM, 0);
12     if (fd == -1)
13         perror("socket"), exit(-1);
14 
15     struct sockaddr_in sock;
16     sock.sin_family = AF_INET;
17     sock.sin_port = htons(2222);
18     sock.sin_addr.s_addr = inet_addr("127.0.0.1");
19 
20     char buf[100];
21 
22     strcpy(buf, "Hello World");
23 
24     int res = sendto(fd, buf, strlen(buf), 0, (struct sockaddr *)&sock, sizeof(sock));
25     printf("發送成功\n");
26 
27     close(fd);
28 
29     return 0;
30 }

 

本章給出的示例代碼過於簡單,以后我會融合信號、多線程和線程同步知識,編寫一個服務器支持多客戶端示例。

 


免責聲明!

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



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