【Linux网络编程学习】socket API(socket、bind、listen、accept、connect)及简单应用
此為牛客Linux C++課程和黑馬Linux系統(tǒng)編程筆記。
1. 什么是socket
所謂 socket(套接字),就是對(duì)網(wǎng)絡(luò)中不同主機(jī)上的應(yīng)用進(jìn)程之間進(jìn)行雙向通信的端點(diǎn)的抽象。
一個(gè)套接字就是網(wǎng)絡(luò)上進(jìn)程通信的一端,提供了應(yīng)用層進(jìn)程利用網(wǎng)絡(luò)協(xié)議交換數(shù)據(jù)的機(jī)制。從所處
的地位來(lái)講,套接字上聯(lián)應(yīng)用進(jìn)程,下聯(lián)網(wǎng)絡(luò)協(xié)議棧,是應(yīng)用程序通過(guò)網(wǎng)絡(luò)協(xié)議進(jìn)行通信的接口,
是應(yīng)用程序與網(wǎng)絡(luò)協(xié)議根進(jìn)行交互的接口。
socket 可以看成是兩個(gè)網(wǎng)絡(luò)應(yīng)用程序進(jìn)行通信時(shí),各自通信連接中的端點(diǎn),這是一個(gè)邏輯上的概
念。它是網(wǎng)絡(luò)環(huán)境中進(jìn)程間通信的 API,也是可以被命名和尋址的通信端點(diǎn),使用中的每一個(gè)套接
字都有其類型和一個(gè)與之相連進(jìn)程。通信時(shí)其中一個(gè)網(wǎng)絡(luò)應(yīng)用程序?qū)⒁獋鬏數(shù)囊欢涡畔?xiě)入它所在
主機(jī)的 socket 中,該 socket 通過(guò)與網(wǎng)絡(luò)接口卡(NIC)相連的傳輸介質(zhì)將這段信息送到另外一臺(tái)
主機(jī)的 socket 中,使對(duì)方能夠接收到這段信息。socket 是由 IP 地址和端口結(jié)合的,提供向應(yīng)用
層進(jìn)程傳送數(shù)據(jù)包的機(jī)制。
socket 本身有“插座”的意思,在 Linux 環(huán)境下,用于表示進(jìn)程間網(wǎng)絡(luò)通信的特殊文件類型。本質(zhì)為
內(nèi)核借助緩沖區(qū)形成的偽文件。既然是文件,那么理所當(dāng)然的,我們可以使用文件描述符引用套接
字。與管道類似的,Linux 系統(tǒng)將其封裝成文件的目的是為了統(tǒng)一接口,使得讀寫(xiě)套接字和讀寫(xiě)文
件的操作一致。區(qū)別是管道主要應(yīng)用于本地進(jìn)程間通信,而套接字多應(yīng)用于網(wǎng)絡(luò)進(jìn)程間數(shù)據(jù)的傳
遞。
套接字的內(nèi)核實(shí)現(xiàn)較為復(fù)雜,不宜在學(xué)習(xí)初期深入學(xué)習(xí)。
在TCP/IP協(xié)議中,“IP地址+TCP或UDP端口號(hào)”唯一標(biāo)識(shí)網(wǎng)絡(luò)通訊中的一個(gè)進(jìn)程。“IP地址+端口號(hào)”就對(duì)應(yīng)一個(gè)socket。欲建立連接的兩個(gè)進(jìn)程各自有一個(gè)socket來(lái)標(biāo)識(shí),那么這兩個(gè)socket組成的socket pair就唯一標(biāo)識(shí)一個(gè)連接。因此可以用Socket來(lái)描述網(wǎng)絡(luò)連接的一對(duì)一關(guān)系。
套接字通信原理如下圖所示:
在網(wǎng)絡(luò)通信中,套接字一定是成對(duì)出現(xiàn)的。一端的發(fā)送緩沖區(qū)對(duì)應(yīng)對(duì)端的接收緩沖區(qū)。我們使用同一個(gè)文件描述符索發(fā)送緩沖區(qū)和接收緩沖區(qū)。
2. socket相關(guān)函數(shù)
2.1 socket( )函數(shù)
#include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> // 包含了這個(gè)頭文件,上面兩個(gè)就可以省略 int socket(int domain, int type, int protocol);功能:創(chuàng)建一個(gè)套接字
參數(shù):
-
domain: 協(xié)議族:
AF_INET : ipv4
AF_INET6 : ipv6
AF_UNIX, AF_LOCAL : 本地套接字通信(進(jìn)程間通信) -
type: 通信過(guò)程中使用的協(xié)議類型:
SOCK_STREAM : 流式協(xié)議
SOCK_DGRAM : 報(bào)式協(xié)議 -
protocol : 具體的一個(gè)協(xié)議。一般寫(xiě)0:
SOCK_STREAM : 流式協(xié)議默認(rèn)使用 TCP
SOCK_DGRAM : 報(bào)式協(xié)議默認(rèn)使用 UDP -
返回值: 成功:返回文件描述符,操作的就是內(nèi)核緩沖區(qū)。失敗:-1
socket( )打開(kāi)一個(gè)網(wǎng)絡(luò)通訊端口,如果成功的話,就像open()一樣返回一個(gè)文件描述符,應(yīng)用程序可以像讀寫(xiě)文件一樣用read/write在網(wǎng)絡(luò)上收發(fā)數(shù)據(jù),如果socket()調(diào)用出錯(cuò)則返回-1。對(duì)于IPv4,domain參數(shù)指定為AF_INET。對(duì)于TCP協(xié)議,type參數(shù)指定為SOCK_STREAM,表示面向流的傳輸協(xié)議。如果是UDP協(xié)議,則type參數(shù)指定為SOCK_DGRAM,表示面向數(shù)據(jù)報(bào)的傳輸協(xié)議。protocol參數(shù)的介紹從略,指定為0即可。
2.2 bind( )函數(shù)
#include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> // 包含了這個(gè)頭文件,上面兩個(gè)就可以省略 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命名功能:綁定,將文件描述符fd和本地的<IP , 端口>進(jìn)行綁定
參數(shù):
- sockfd : 通過(guò)socket()函數(shù)得到的文件描述符
- addr : 需要綁定的socket地址,這個(gè)地址封裝了ip和端口號(hào)的信息
- addrlen : 第二個(gè)參數(shù)結(jié)構(gòu)體占的內(nèi)存大小
返回值:成功返回0,失敗返回-1, 設(shè)置errno
服務(wù)器程序所監(jiān)聽(tīng)的網(wǎng)絡(luò)地址和端口號(hào)通常是固定不變的,客戶端程序得知服務(wù)器程序的地址和端口號(hào)后就可以向服務(wù)器發(fā)起連接,因此服務(wù)器需要調(diào)用bind綁定一個(gè)固定的網(wǎng)絡(luò)地址和端口號(hào)。
bind()的作用是將參數(shù)sockfd和addr綁定在一起,使sockfd這個(gè)用于網(wǎng)絡(luò)通訊的文件描述符監(jiān)聽(tīng)addr所描述的地址和端口號(hào)。上一篇講過(guò),struct sockaddr *是一個(gè)通用指針類型,addr參數(shù)實(shí)際上可以接受多種協(xié)議的sockaddr結(jié)構(gòu)體,而它們的長(zhǎng)度各不相同,所以需要第三個(gè)參數(shù)addrlen指定結(jié)構(gòu)體的長(zhǎng)度。
2.3 listen( )函數(shù)
#include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> // 包含了這個(gè)頭文件,上面兩個(gè)就可以省略 int listen(int sockfd, int backlog);功能:監(jiān)聽(tīng)這個(gè)socket上的連接
參數(shù):
- sockfd : 通過(guò)socket()函數(shù)得到的文件描述符
- backlog : 未連接的和已經(jīng)連接的和的最大值(排隊(duì)建立3次握手隊(duì)列和剛剛建立3次握手隊(duì)列的鏈接數(shù)和的最大值)
典型的服務(wù)器程序可以同時(shí)服務(wù)于多個(gè)客戶端,當(dāng)有客戶端發(fā)起連接時(shí),服務(wù)器調(diào)用的accept()返回并接受這個(gè)連接,如果有大量的客戶端發(fā)起連接而服務(wù)器來(lái)不及處理,尚未accept的客戶端就處于連接等待狀態(tài),listen()聲明sockfd處于監(jiān)聽(tīng)狀態(tài),并且最多允許有backlog個(gè)客戶端處于連接待狀態(tài),如果接收到更多的連接請(qǐng)求就忽略。listen()成功返回0,失敗返回-1。
2.4 accept( )函數(shù)
#include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> // 包含了這個(gè)頭文件,上面兩個(gè)就可以省略 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);功能:接收客戶端連接,默認(rèn)是一個(gè)阻塞的函數(shù),阻塞等待客戶端連接
參數(shù):
- sockfd : 用于監(jiān)聽(tīng)的文件描述符
- addr : 傳出參數(shù),記錄了連接成功后客戶端的地址信息(ip,port)
- addrlen : 傳入傳出參數(shù)(值-結(jié)果), 指定第二個(gè)參數(shù)的對(duì)應(yīng)的內(nèi)存大小,傳出第二個(gè)參數(shù)傳出時(shí)的內(nèi)存大小
返回值:
成功:返回一個(gè)新的socket文件描述符,用于和客戶端通信,失敗返回-1,設(shè)置errno
三次握手完成后,服務(wù)器調(diào)用accept()接受連接,如果服務(wù)器調(diào)用accept()時(shí)還沒(méi)有客戶端的連接請(qǐng)求,就阻塞等待直到有客戶端連接上來(lái)。addr是一個(gè)傳出參數(shù),accept()返回時(shí)傳出客戶端的地址和端口號(hào)。addrlen參數(shù)是一個(gè)傳入傳出參數(shù)(value-result argument),傳入的是調(diào)用者提供的緩沖區(qū)addr的長(zhǎng)度以避免緩沖區(qū)溢出問(wèn)題,傳出的是客戶端地址結(jié)構(gòu)體的實(shí)際長(zhǎng)度(有可能沒(méi)有占滿調(diào)用者提供的緩沖區(qū))。如果給addr參數(shù)傳NULL,表示不關(guān)心客戶端的地址。
一般來(lái)說(shuō),我們的服務(wù)器程序結(jié)構(gòu)是這樣的:
while (1) {cliaddr_len = sizeof(cliaddr);connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);n = read(connfd, buf, MAXLINE);......close(connfd); }整個(gè)是一個(gè)while死循環(huán),每次循環(huán)處理一個(gè)客戶端連接。由于cliaddr_len是傳入傳出參數(shù),每次調(diào)用accept()之前應(yīng)該重新賦初值。accept()的參數(shù)listenfd是先前的監(jiān)聽(tīng)文件描述符,而accept()的返回值是另外一個(gè)文件描述符connfd,之后與客戶端之間就通過(guò)這個(gè)connfd通訊,最后關(guān)閉connfd斷開(kāi)連接,而不關(guān)閉listenfd,再次回到循環(huán)開(kāi)頭listenfd仍然用作accept的參數(shù)。accept()成功返回一個(gè)文件描述符,出錯(cuò)返回-1。
一定要區(qū)分開(kāi)connfd和listenfd的作用,listenfd僅用于監(jiān)聽(tīng),監(jiān)聽(tīng)到了以后并不用它來(lái)進(jìn)行信息傳輸。
2.5 connect( )函數(shù)
#include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> // 包含了這個(gè)頭文件,上面兩個(gè)就可以省略 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);功能: 客戶端連接服務(wù)器
參數(shù):
- sockfd : 用于通信的文件描述符
- addr : 客戶端要連接的服務(wù)器的地址信息
- addrlen : 第二個(gè)參數(shù)的內(nèi)存大小
返回值:成功 0, 失敗 -1
客戶端需要調(diào)用connect()連接服務(wù)器,connect和bind的參數(shù)形式一致,區(qū)別在于bind的參數(shù)是自己的地址,而connect的參數(shù)是對(duì)方的地址。
3. 實(shí)現(xiàn)一個(gè)簡(jiǎn)單的服務(wù)端-客戶端通信
接下來(lái)使用以上函數(shù)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的服務(wù)端客戶端通信,實(shí)現(xiàn)將客戶端的小寫(xiě)字母轉(zhuǎn)換為大寫(xiě)字母。
3.1 通信流程
上圖給了一個(gè)C/S模型網(wǎng)絡(luò)編程的socket模板。
服務(wù)端:
客戶端:
其中還有很多細(xì)節(jié),需要寫(xiě)代碼的時(shí)候才能體會(huì)。
3.2 服務(wù)端
/*實(shí)現(xiàn)一個(gè)簡(jiǎn)單的服務(wù)器-客戶端通信*/#include <stdio.h> #include <unistd.h> #include <arpa/inet.h> #include <stdlib.h>// 設(shè)定一個(gè)服務(wù)器端口號(hào) #define SERV_IP "127.0.0.1" #define SERV_PORT 6666int main() {int lfd; // 用于監(jiān)聽(tīng)的socket的文件描述符,真正用于通信的套接字是接下來(lái)accept函數(shù)返回的cfd套接字struct sockaddr_in serv_addr;lfd = socket(AF_INET, SOCK_STREAM, 0);serv_addr.sin_family = AF_INET;// 注意這里,要把小端存儲(chǔ)的端口號(hào)改為大端存儲(chǔ)serv_addr.sin_port = htons(SERV_PORT); inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr); // 調(diào)用ip轉(zhuǎn)換函數(shù),把字符串ip轉(zhuǎn)化為網(wǎng)絡(luò)字節(jié)序bind(lfd, (struct sockaddr * )&serv_addr, sizeof(serv_addr));listen(lfd, 128); // 最大連接與待連接數(shù)設(shè)為128int cfd; // 已連接的客戶端的socket的文件描述符, 以便一會(huì)兒read用struct sockaddr_in clie_addr; // 作為accept的第二個(gè)參數(shù),為傳出參數(shù),傳出的是客戶端的sockadd_insocklen_t clie_addr_len = sizeof(clie_addr); // 作為accept的第三個(gè)參數(shù),為傳入傳出參數(shù),之所以要單獨(dú)定義出來(lái)是因?yàn)橐獋鞒?/span>cfd = accept(lfd, (struct sockaddr *)&clie_addr, &clie_addr_len);while(1) {// 此處輸出連接的客戶端的ip和端口char clie_IP[BUFSIZ];printf("Client IP: %s, client port: %d\n", inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)),ntohs(clie_addr.sin_port));char buf[BUFSIZ]; // 給read使用,存儲(chǔ)讀出的數(shù)據(jù),BUFSIZ宏是系統(tǒng)用來(lái)專門(mén)給buf賦長(zhǎng)度的宏,為8k(Default buffer size)int len; // read的返回值,是讀入字符的長(zhǎng)度len = read(cfd, buf, sizeof(buf));// 把小寫(xiě)字母轉(zhuǎn)化為大寫(xiě)字母int i;for(i = 0; i < len; ++i) {if(buf[i] <= 'z' && buf[i] >= 'a') {buf[i] -= 32;}}write(cfd, buf, len);}close(lfd);close(cfd);return 0; }以上代碼為突出主體,沒(méi)有寫(xiě)錯(cuò)誤判斷與錯(cuò)誤提示。帶錯(cuò)誤提示代碼如下:
/*實(shí)現(xiàn)一個(gè)簡(jiǎn)單的服務(wù)器-客戶端通信*/#include <stdio.h> #include <unistd.h> #include <arpa/inet.h> #include <stdlib.h>// 設(shè)定一個(gè)服務(wù)器端口號(hào) #define SERV_IP "127.0.0.1" #define SERV_PORT 6666int main() {int lfd; // 用于監(jiān)聽(tīng)的socket的文件描述符struct sockaddr_in serv_addr;int ret; // 用于錯(cuò)誤檢測(cè)lfd = socket(AF_INET, SOCK_STREAM, 0);if(lfd == -1) {perror("socket error");exit(1);}serv_addr.sin_family = AF_INET;// 注意這里,要把小端存儲(chǔ)的端口號(hào)改為大端存儲(chǔ)serv_addr.sin_port = htons(SERV_PORT); inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr); // 調(diào)用ip轉(zhuǎn)換函數(shù),把字符串ip轉(zhuǎn)化為網(wǎng)絡(luò)字節(jié)序ret = bind(lfd, (struct sockaddr * )&serv_addr, sizeof(serv_addr));if(ret == -1) {perror("bind error");exit(1);}ret = listen(lfd, 128); // 最大連接與待連接數(shù)設(shè)為128if(ret == -1) {perror("listen error");exit(1);}int cfd; // 已連接的客戶端的socket的文件描述符, 以便一會(huì)兒read用struct sockaddr_in clie_addr; // 作為accept的第二個(gè)參數(shù),為傳出參數(shù),傳出的是客戶端的sockadd_insocklen_t clie_addr_len = sizeof(clie_addr); // 作為accept的第三個(gè)參數(shù),為傳入傳出參數(shù),之所以要單獨(dú)定義出來(lái)是因?yàn)橐獋鞒?/span>cfd = accept(lfd, (struct sockaddr *)&clie_addr, &clie_addr_len);if(cfd == -1) {perror("accept error");exit(1);}// 此處輸出連接的客戶端的ip和端口char clie_IP[BUFSIZ];printf("Client IP: %s, client port: %d\n", inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)),ntohs(clie_addr.sin_port));while(1) {char buf[BUFSIZ]; // 給read使用,存儲(chǔ)讀出的數(shù)據(jù),BUFSIZ宏是系統(tǒng)用來(lái)專門(mén)給buf賦長(zhǎng)度的宏,為8k(Default buffer size)int len; // read的返回值,是讀入字符的長(zhǎng)度len = read(cfd, buf, sizeof(buf));if(len == -1) {perror("read error");exit(1);}// 把小寫(xiě)字母轉(zhuǎn)化為大寫(xiě)字母int i;for(i = 0; i < len; ++i) {if(buf[i] <= 'z' && buf[i] >= 'a') {buf[i] -= 32;}}ret = write(cfd, buf, len);if(ret == -1) {perror("write error");exit(1);}}close(cfd);close(lfd);return 0; }3.3 客戶端
#include <stdio.h> #include <unistd.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <netinet/in.h>// 服務(wù)器的ip和端口 #define SERV_IP "127.0.0.1" #define SERV_PORT 6666int main() {int cfd; // 用于寫(xiě)入數(shù)據(jù)傳輸給服務(wù)端的socket的文件描述符cfd = socket(AF_INET, SOCK_STREAM, 0);// bind() 可以不調(diào)用bind(), linux會(huì)隱式地綁定struct sockaddr_in serv_addr; // 因?yàn)橐B接服務(wù)端,這里的sockadd_in是用于指定服務(wù)端的ip和端口bzero(&serv_addr, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(SERV_PORT);inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr); // 調(diào)用ip轉(zhuǎn)換函數(shù),把字符串ip轉(zhuǎn)化為網(wǎng)絡(luò)字節(jié)序connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));while(1) {// 從終端讀取內(nèi)容char buf[BUFSIZ];fgets(buf, sizeof(buf), stdin); // 讀一行// 寫(xiě)入到cfd中,傳輸給服務(wù)端write(cfd, buf, strlen(buf)); // 注意不要寫(xiě)成sizeof(buf),sizeof是在內(nèi)存中所占的大小,strlen是到第一個(gè)'\0'位止。// read在讀socket時(shí)默認(rèn)時(shí)阻塞的,阻塞等待服務(wù)端傳輸數(shù)據(jù)int len;len = read(cfd, buf, sizeof(buf));printf("%s", buf);}close(cfd);return 0; }以上代碼為突出主體,沒(méi)有寫(xiě)錯(cuò)誤判斷與錯(cuò)誤提示。帶錯(cuò)誤提示代碼如下:
#include <stdio.h> #include <unistd.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <netinet/in.h>// 服務(wù)器的ip和端口 #define SERV_IP "127.0.0.1" #define SERV_PORT 6666int main() {int ret; // 用于錯(cuò)誤檢測(cè)int cfd; // 用于寫(xiě)入數(shù)據(jù)傳輸給服務(wù)端的socket的文件描述符cfd = socket(AF_INET, SOCK_STREAM, 0);if(cfd == -1) {perror("socket error");exit(1);}// bind() 可以不調(diào)用bind(), linux會(huì)隱式地綁定struct sockaddr_in serv_addr; // 因?yàn)橐B接服務(wù)端,這里的sockadd_in是用于指定服務(wù)端的ip和端口bzero(&serv_addr, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(SERV_PORT);inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr); // 調(diào)用ip轉(zhuǎn)換函數(shù),把字符串ip轉(zhuǎn)化為網(wǎng)絡(luò)字節(jié)序ret = connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));if(ret == -1) {perror("connect error");exit(1);}// 從終端讀取內(nèi)容while(1) {char buf[BUFSIZ];fgets(buf, sizeof(buf), stdin); // 讀一行// 寫(xiě)入到cfd中,傳輸給服務(wù)端ret = write(cfd, buf, strlen(buf)); // 注意不要寫(xiě)成sizeof(buf),sizeof是在內(nèi)存中所占的大小,strlen是到第一個(gè)'\0'位止。if(ret == -1) {perror("write error");exit(1);}// read在讀socket時(shí)默認(rèn)時(shí)阻塞的,阻塞等待服務(wù)端傳輸數(shù)據(jù)int len;len = read(cfd, buf, sizeof(buf));if(len == -1) {perror("read error");exit(1);}printf("%s", buf);}close(cfd);return 0; }3.4 程序運(yùn)行結(jié)果及注意事項(xiàng)
先啟動(dòng)server后啟動(dòng)client,在client的終端輸入小寫(xiě)字符后,可見(jiàn)翻譯成了大寫(xiě):
同時(shí)在server端輸出了client的ip和端口號(hào)
程序有兩個(gè)注意事項(xiàng):
總結(jié)
以上是生活随笔為你收集整理的【Linux网络编程学习】socket API(socket、bind、listen、accept、connect)及简单应用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 输卵管炎症的症状有什么
- 下一篇: 【Linux网络编程学习】使用socke