socket 簡介
Socket即套接字,就是對網絡中不同主機上的應用進程之間進行雙向通信的端點的抽象。一個套接字就是網絡上進程通信的一端,提供了應用層進程利用網絡協議交換數據的機制。從所處的地位來講,套接字上聯應用進程,下聯網絡協議棧,是應用程序通過網絡協議進行通信的接口。
socket起源于UNIX,在Unix一切皆文件哲學的思想下,socket是一種"打開—讀/寫—關閉"模式的實現,服務器和客戶端各自維護一個"文件",在建立連接打開后,可以向自己文件寫入內容供對方讀取或者讀取對方內容,通訊結束時關閉文件。
通信流程
socket是"打開—讀/寫—關閉"模式的實現,以使用TCP協議通訊的socket為例,其交互流程如下:
TCP編程步驟:
TCP服務器端一般步驟是:
1、創建一個socket,用函數socket();
2、設置socket屬性,用函數setsockopt(); 可選
3、綁定IP地址、端口等信息到socket上,用函數bind();
4、開啟監聽,用函數listen();
5、接收客戶端上來的連接,用函數accept();
6、收發數據,用函數send()和recv(),或者read()和write();
7、關閉網絡連接;
8、關閉監聽;
TCP客戶端一般步驟是:
1、創建一個socket,用函數socket();
2、設置socket屬性,用函數setsockopt();可選
3、綁定IP地址、端口等信息到socket上,用函數bind();* 可選
4、設置要連接的對方的IP地址和端口等屬性;
5、連接服務器,用函數connect();
6、收發數據,用函數send()和recv(),或者read()和write();
7、關閉網絡連接;
UDP的編程步驟:
UDP服務器端一般步驟是:
1、創建一個socket,用函數socket();
2、設置socket屬性,用函數setsockopt();可選
3、綁定IP地址、端口等信息到socket上,用函數bind();
4、循環接收數據,用函數recvfrom();
5、關閉網絡連接;
UDP客戶端一般步驟是:
1、創建一個socket,用函數socket();
2、設置socket屬性,用函數setsockopt();可選
3、綁定IP地址、端口等信息到socket上,用函數bind();可選
4、設置對方的IP地址和端口等屬性;
5、發送數據,用函數sendto();
6、關閉網絡連接;
linux下socket程序示例
以TCP協議為例,實現的功能是:客戶端從服務器讀取一個字符串并打印出來。
服務器端代碼 server.cpp:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(){//創建套接字int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//將套接字和IP、端口綁定struct sockaddr_in serv_addr;memset(&serv_addr, 0, sizeof(serv_addr)); //每個字節都用0填充serv_addr.sin_family = AF_INET; //使用IPv4地址serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具體的IP地址serv_addr.sin_port = htons(1234); //端口bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));//進入監聽狀態,等待用戶發起請求listen(serv_sock, 20);//接收客戶端請求struct sockaddr_in clnt_addr;socklen_t clnt_addr_size = sizeof(clnt_addr);int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);//向客戶端發送數據char str[] = "Hello World!";write(clnt_sock, str, sizeof(str));//關閉套接字close(clnt_sock);close(serv_sock);return 0;
}
客戶端代碼 client.cpp:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main(){//創建套接字int sock = socket(AF_INET, SOCK_STREAM, 0);//向服務器(特定的IP和端口)發起請求struct sockaddr_in serv_addr;memset(&serv_addr, 0, sizeof(serv_addr)); //每個字節都用0填充serv_addr.sin_family = AF_INET; //使用IPv4地址serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具體的IP地址serv_addr.sin_port = htons(1234); //端口connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));//讀取服務器傳回的數據char buffer[40];read(sock, buffer, sizeof(buffer)-1);printf("Message form server: %s\n", buffer);//關閉套接字close(sock);return 0;
}
先編譯 server.cpp 并運行:
[admin@localhost ~]$ g++ server.cpp -o server
[admin@localhost ~]$ ./server
正常情況下,程序運行到 accept() 函數就會被阻塞,等待客戶端發起請求。
接下來編譯 client.cpp 并運行:
[admin@localhost ~]$ g++ client.cpp -o client
[admin@localhost ~]$ ./client
Message form server: Hello World!
[admin@localhost ~]$
client 運行后,通過 connect() 函數向 server 發起請求,處于監聽狀態的 server 被激活,執行 accept() 函數,接受客戶端的請求,然后執行 write() 函數向 client 傳回數據。client 接收到傳回的數據后,connect() 就運行結束了,然后使用 read() 將數據讀取出來。
需要注意的是:
server 只接受一次 client 請求,當 server 向 client 傳回數據后,程序就運行結束了。如果想再次接收到服務器的數據,必須再次運行 server,所以這是一個非常簡陋的 socket 程序,不能夠一直接受客戶端的請求。
源碼解析
- 先說一下 server.cpp 中的代碼。
第11行通過 socket() 函數創建了一個套接字,參數 AF_INET 表示使用 IPv4 地址,SOCK_STREAM 表示使用面向連接的數據傳輸方式,IPPROTO_TCP 表示使用 TCP 協議。在 Linux 中,socket 也是一種文件,有文件描述符,可以使用 write() / read() 函數進行 I/O 操作。
第19行通過 bind() 函數將套接字 serv_sock 與特定的IP地址和端口綁定,IP地址和端口都保存在 sockaddr_in 結構體中。socket() 函數確定了套接字的各種屬性,bind() 函數讓套接字與特定的IP地址和端口對應起來,這樣客戶端才能連接到該套接字。
第22行讓套接字處于被動監聽狀態。所謂被動監聽,是指套接字一直處于“睡眠”中,直到客戶端發起請求才會被“喚醒”。
第27行的 accept() 函數用來接收客戶端的請求。程序一旦執行到 accept() 就會被阻塞(暫停運行),直到客戶端發起請求。
第31行的 write() 函數用來向套接字文件中寫入數據,也就是向客戶端發送數據。
和普通文件一樣,socket 在使用完畢后也要用 close() 關閉。
- 再說一下 client.cpp 中的代碼。client.cpp 中的代碼和 server.cpp 中有一些區別。
第19行代碼通過 connect() 向服務器發起請求,服務器的IP地址和端口號保存在 sockaddr_in 結構體中。直到服務器傳回數據后,connect() 才運行結束。
第23行代碼通過 read() 從套接字文件中讀取數據。