更好閱讀體驗:《理解 TCP 和 UDP》— By Gitbook javascript
咱們已經知道網絡中的進程是經過 socket 來通訊的,那什麼是 socket 呢?
socket 起源於 UNIX,而 UNIX/Linux 基本哲學之一就是「一切皆文件」,均可以用「open → write/read → close」模式來操做。
socket 其實就是該模式的一個實現,socket 便是一種特殊的文件,一些 socket 函數就是對其進行的操做。 java
使用 TCP/IP 協議的應用程序一般採用系統提供的編程接口:UNIX BSD 的套接字接口(Socket Interfaces)
以此來實現網絡進程之間的通訊。
就目前而言,幾乎全部的應用程序都是採用 socket,因此說如今的網絡時代,網絡中進程通訊是無處不在,一切皆 socket c++
套接字接口是一組函數,由操做系統提供,用以建立網絡應用。
大多數現代操做系統都實現了套接字接口,包括全部 Unix 變種,Windows 和 Macintosh 系統。 git
套接字接口的起源
套接字接口是加州大學伯克利分校的研究人員在 20 世紀 80 年代早起提出的。
伯克利的研究者使得套接字接口適用於任何底層的協議,第一個實現就是針對 TCP/IP 協議,他們把它包括在 Unix 4.2 BSD 的內核裏,而且分發給許多學校和實驗室。
這在因特網的歷史成爲了一個重大事件。
—— 《深刻理解計算機系統》編程
從 Linux 內核的角度來看,一個套接字就是通訊的一個端點。
從 Linux 程序的角度來看,套接字是一個有相應描述符的文件。
普通文件的打開操做返回一個文件描述字,而 socket() 用於建立一個 socket 描述符,惟一標識一個 socket。
這個 socket 描述字跟文件描述字同樣,後續的操做都有用到它,把它做爲參數,經過它來進行一些操做。 服務器
經常使用的函數有:網絡
圖中展現了 TCP 協議的 socket 交互流程,描述以下:數據結構
這個過程當中,服務器和客戶端創建鏈接的部分,就體現了 TCP 三次握手的原理。 dom
下面詳細講一下 socket 的各函數。 socket
socket 是系統提供的接口,而操做系統大多數都是用 C/C++ 開發的,天然函數庫也是 C/C++ 代碼。
該函數會返回一個套接字描述符(socket descriptor),可是該描述符僅是部分打開的,還不能用於讀寫。
如何完成打開套接字的工做,取決於咱們是客戶端仍是服務器。
#include <sys/socket.h>
int socket(int domain, int type, int protocol);複製代碼
domain:
協議域,決定了 socket 的地質類型,在通訊中必須採用對應的地址。
經常使用的協議族有:AF_INET
(ipv4地址與端口號的組合)、AF_INET6
(ipv6地址與端口號的組合)、AF_LOCAL
(絕對路徑名做爲地址)。
該值的常量定義在 sys/socket.h
文件中。
type:
指定 socket 類型。
經常使用的類型有:SOCK_STREAM
、SOCK_DGRAM
、SOCK_RAW
、SOCK_PACKET
、SOCK_SEQPACKET
等。
其中 SOCK_STREAM
表示提供面向鏈接的穩定數據傳輸,即 TCP 協議。
該值的常量定義在 sys/socket.h
文件中。
protocol:
指定協議。
經常使用的協議有:IPPROTO_TCP
(TCP協議)、IPPTOTO_UDP
(UDP協議)、IPPROTO_SCTP
(STCP協議)。
當值位 0 時,會自動選擇 type
類型對應的默認協議。
由服務端調用,把一個地址族中的特定地址和 socket 聯繫起來。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);複製代碼
sockfd:
即 socket 描述字,由 socket() 函數建立。
*addr:
一個 const struct sockaddr
指針,指向要綁定給 sockfd
的協議地址。
這個地址結構根據地址建立 socket 時的地址協議族不一樣而不一樣,例如 ipv4 對應 sockaddr_in
,ipv6 對應 sockaddr_in6
.
這幾個結構體在使用的時候,均可以強制轉換成 sockaddr
。
下面是這幾個結構體對應的所在的頭文件:
sockaddr
: sys/socket.h
sockaddr_in
: netinet/in.h
sockaddr_in6
: netinet6/in.h
_in 後綴意義:互聯網絡(internet)的縮寫,而不是輸入(input)的縮寫。
服務器調用,將 socket 從一個主動套接字轉化爲一個監聽套接字(listening socket), 該套接字能夠接收來自客戶端的鏈接請求。
在默認狀況下,操做系統內核會認爲 socket 函數建立的描述符對應於主動套接字(active socket)。
#include <sys/socket.h>
int listen(int sockfd, int backlog);複製代碼
sockfd:
即 socket 描述字,由 socket() 函數建立。
backlog:
指定在請求隊列中的最大請求數,進入的鏈接請求將在隊列中等待 accept() 它們。
由客戶端調用,與目的服務器的套接字創建一個鏈接。
#include <sys/socket.h>
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);複製代碼
clientfd:
目的服務器的 socket 描述符
*addr:
一個 const struct sockaddr
指針,包含了目的服務器 IP 和端口。
addrlen:
協議地址的長度,若是是 ipv4 的 TCP 鏈接,通常爲 sizeof(sockaddr_in)
;
服務器調用,等待來自客戶端的鏈接請求。
當客戶端鏈接,accept 函數會在 addr
中會填充上客戶端的套接字地址,而且返回一個已鏈接描述符(connected descriptor),這個描述符能夠用來利用 Unix I/O 函數與客戶端通訊。
#indclude <sys/socket.h>
int accept(int listenfd, struct sockaddr *addr, int *addrlen);複製代碼
listenfd:
服務器的 socket 描述字,由 socket() 函數建立。
*addr:
一個 const struct sockaddr
指針,用來存放提出鏈接請求客戶端的主機的信息
*addrlen:
協議地址的長度,若是是 ipv4 的 TCP 鏈接,通常爲 sizeof(sockaddr_in)
。
在數據傳輸完成以後,手動關閉鏈接。
#include <sys/socket.h>
#include <unistd.h>
int close(int fd);複製代碼
fd:
須要關閉的鏈接 socket 描述符
當客戶端和服務器創建鏈接後,可使用網絡 I/O 進行讀寫操做。
網絡 I/O 操做有下面幾組:
最經常使用的是 read()/write()
他們的原型是:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);複製代碼
鑑於該文是側重於描述 socket 的工做原理,就再也不詳細描述這些函數了。
// socket_server.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#define MAXLINE 4096 // 4 * 1024
int main(int argc, char **argv) {
int listenfd, // 監聽端口的 socket 描述符
connfd; // 鏈接端 socket 描述符
struct sockaddr_in servaddr;
char buff[MAXLINE];
int n;
// 建立 socket,而且進行錯誤處理
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
// 初始化 sockaddr_in 數據結構
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666);
// 綁定 socket 和 端口
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
{
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
// 監聽鏈接
if (listen(listenfd, 10) == -1)
{
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("====== Waiting for client's request======\n");
// 持續接收客戶端的鏈接請求
while (true)
{
if ((connfd = accept(listenfd, (struct sockaddr *)NULL, NULL) == -1))
{
printf("accept socket error: %s(errno: %d)\n", 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);
return 0;
}複製代碼
// socket_client.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.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");
return 0;
}
// 建立 socket 描述符
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
// 初始化目標服務器數據結構
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(6666);
// 從參數中讀取 IP 地址
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
{
printf("inet_pton error for %s\n", argv[1]);
return 0;
}
// 鏈接目標服務器,並和 sockfd 聯繫起來。
if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
printf("connect error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("send msg to server: \n");
// 從標準輸入流中讀取信息
fgets(sendline, 4096, stdin);
// 經過 sockfd,向目標服務器發送信息
if (send(sockfd, sendline, strlen(sendline), 0) < 0)
{
printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
// 數據傳輸完畢,關閉 socket 鏈接
close(sockfd);
return 0;
}複製代碼
首先建立 makefile
文件
all:server client
server:socket_server.o
g++ -g -o socket_server socket_server.o
client:socket_client.o
g++ -g -o socket_client socket_client.o
socket_server.o:socket_server.cpp
g++ -g -c socket_server.cpp
socket_client.o:socket_client.cpp
g++ -g -c socket_client.cpp
clean:all
rm all複製代碼
而後使用命令:
$ make複製代碼
會生成兩個可執行文件:
socket_server
socket_client
分別打開兩個終端,運行:
./socket_server
./socket_client 127.0.0.1
而後在 socket_client
中鍵入發送內容,能夠再 socket_server
接收到一樣的信息。