參考
http://c.biancheng.net/cpp/socket/
本教程不要求讀者有Linux和Windows開發(fā)經(jīng)驗, 也不需要深入了解 TCP/IP 協(xié)議, 涉及到相關知識時都會說明
同時學 Linux和Windows 的原因
大多項目在 Windows/Linux 下開發(fā) client/server 端, 單獨學1種平臺 沒實踐意義
兩大平臺下 socket 編程非常相似
網(wǎng)絡編程 就是 編寫程序 使兩臺聯(lián)網(wǎng)的計算機 相互交換數(shù)據(jù), 這就是 socket 全部內(nèi)容 嗎?是的!
socket 編程 遠比想象中簡單
chapter1 socket 簡介
socket: 套接字, 計算機間通信的一種 約定
socket 典型應用: Web 服務器 和 瀏覽器
瀏覽器
獲取 用戶輸入的 URL
向 服務器 發(fā)起請求
服務器
分析收到的 URL
將對應的網(wǎng)頁內(nèi)容返回給瀏覽器
瀏覽器
解析和渲染, 將文字、圖片、視頻 呈現(xiàn)給用戶
1 IP Address
(1) 封裝 到要發(fā)送的數(shù)據(jù)包
(2) 被 路由器 用于 尋址: 據(jù) IP Address 找到 dst 計算機
本機地址: 127.0.0.1
2 Port
(1) 用于 區(qū)分 不同的 網(wǎng)絡程序
網(wǎng)絡程序 端口號
Web 服務 80
FTP 服務 21
SMTP 服務 25
(2) 是 虛擬/邏輯 概念
可視為 一道門, data 通過這道門 流入流出, 每道門有不同的 編號, 即 端口號
3 Protocol
網(wǎng)絡通信的雙方遵守的約定
TCP/IP 協(xié)議族: TCP IP UDP Telnet FTP SMTP 等上百個關聯(lián)協(xié)議
TCP IP 常用
4 數(shù)據(jù)傳輸方式
常用2種:
(1) SOCK_STREAM
面向連接
重發(fā)
http 協(xié)議用
(2) SOCK_DGRAM
無連接
不作數(shù)據(jù)校驗
錯了不重發(fā)
QQ 視頻/語音聊天 用
總結
IP Address 和 Port 能在互聯(lián)網(wǎng)中 定位到要通信的程序
Protocol 和 數(shù)據(jù)傳輸方式 規(guī)定了 如何傳輸 數(shù)據(jù)
有了這些, 兩臺計算機就可以通信了
chapter2 Linux 下 socket 程序 Demo
功能: client 從 server 讀1個字符串, 并打印出來
Linux 中, socket 也是文件, 有文件描述符, 可用 write() / read() 進行 I/O
// 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()
{
// [1] 建 套接字: IPv4 地址 / 面向連接的傳輸方式 / TCP 協(xié)議
int listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Note: 指定 本端(Server) 協(xié)議族 / port / ipaddr
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr) );
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 具體的IP地址
serv_addr.sin_port = htons(1234); // 端口
// [2] bind 套接字 和 IPAddr/Port
bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr) );
printf("listening:...\n");
// [3] 監(jiān)聽: 將 `普通套接字` 轉化為 `監(jiān)聽套接字`
listen(listenfd, 20); // connnection queue 最大 size
struct sockaddr_in clientAddr;
socklen_t clientAddrSize = sizeof(clientAddr);
printf("accepting:...\n");
// [4] 接收 client request:
// 1] 阻塞 thread, 直到 client connection 到達 + 被 OS kernel 接受
// 2] 3次握手: 建立連接
// 3] 返回 `已連接套接字`
int connfd = accept(listenfd, (struct sockaddr*)&clientAddr, &clientAddrSize);
// [5] 發(fā) 數(shù)據(jù) 給 client: 向 套接字文件 寫 數(shù)據(jù)
char str[] = "Hello World!";
write(connfd, str, sizeof(str) );
// [6] 關 套接字
close(connfd);
close(listenfd);
}
// client.cpp
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main()
{
// [1] 建 套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// Note: 指定 對端(Server) 協(xié)議族 / port / ipaddr
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(1234);
// [2] request to server, 直到 server 傳回數(shù)據(jù)后, connect() 才 return
connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr) );
// [3] 讀 對端的 Response
char buffer[40];
read(sockfd, buffer, sizeof(buffer)-1 );
printf("Message form server: %s\n", buffer);
// [4] 關 套接字
close(sockfd);
}
Note: server 只接受1次 client 請求, server 向 client 傳回數(shù)據(jù)后, 程序運行結束
(1) 在1個終端 編譯 -> 運行 server -> accept() 阻塞
$ g++ server.cpp -o server
$ ./server
listening:...
accepting:...
(2) 在1個終端 編譯 -> 運行 client
$ g++ client.cpp -o client
$ ./client
Message form server: Hello World!
chapter3 Windows 下 socket 程序 Demo
server.cpp / client.cpp 分別編譯 為 server.exe / client.exe
先運行 server.exe
>server.exe
listening:...
accepting:...
再運行 client.exe
>client.exe
Message form server: Hello World!
linux/Windows 下 主要區(qū)別:
(1) Windows 下 socket 程序依賴 動態(tài)鏈接庫 Winsock.dll 或 ws2_32.dll,必須提前加載
(2) Linux 用 "文件描述符", Windows 用 "文件句柄"
Linux 不區(qū)分 socket 文件和普通文件, 而 Windows 區(qū)分
Linux/Windows 下 socket() 返回 int/SOCKET 型(句柄)
// server.cpp
#include <stdio.h>
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib") // load ws2_32.dll
int main()
{
// init DLL
WSADATA wsaData;
WSAStartup( MAKEWORD(2, 2), &wsaData);
// (1)
SOCKET listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr));
sockAddr.sin_family = PF_INET;
sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
sockAddr.sin_port = htons(1234);
// (2)
bind(listenfd, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
printf("listening:...\n");
// (3)
listen(listenfd, 20);
SOCKADDR clientAddr;
int clientAddrSize = sizeof(SOCKADDR);
printf("accepting:...\n");
// (4)
SOCKET connfd = accept(listenfd, (SOCKADDR*)&clientAddr, &clientAddrSize);
const char *str = "Hello World!";
// (5)
send(connfd, str, strlen(str)+sizeof(char), NULL);
// (6)
closesocket(connfd);
closesocket(listenfd);
// terminate DLL
WSACleanup();
}
// client.cpp
// client.cpp
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main()
{
// [1] 建 套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// Note: 指定 對端(Server) 協(xié)議族 / port / ipaddr
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(1234);
// [2] request to server, 直到 server 傳回數(shù)據(jù)后, connect() 才 return
connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr) );
// [3] 讀 對端的 Response
char buffer[40];
read(sockfd, buffer, sizeof(buffer)-1 );
printf("Message form server: %s\n", buffer);
// [4] 關 套接字
close(sockfd);
}
chapter4 socket()
1 文件
Linux 中, 一切都是文件: 極大地簡化了程序員的理解和操作, 使得對 硬件設備 的處理 像 普通文件一樣
文本文件
源文件
二進制文件
硬件設備 可被 `映射` 為 虛擬文件/設備文件
鍵盤 <-> stdin 標準輸入文件
顯示器 <-> stdout 標準輸出文件
socket
所有 文件 都可用 read()/write() 讀/寫數(shù)據(jù)
創(chuàng)建的文件都有1個 int 型編號, 即 文件描述符(File Descriptor) - Windows 下稱 文件句柄*File Handle)
使用文件 時, 只要知道 文件描述符 即可
stdin 描述符 0
stdout 描述符 1
2臺計算機間 1次 socket 通信, 實際上是 server/client 對 1個 socket 文件 的 1次 寫/讀
2 Linux 建 socket
<sys/socket.h>
int socket(int af, int type, int protocol);
af: 地址族(Address Family)
AF_INET / AF_INET6 (IPv4 / IPv6 地址)
type 數(shù)據(jù)傳輸方式
SOCK_STREAM / SOCK_DGRAM
protocol 傳輸協(xié)議
IPPROTO_TCP / IPPTOTO_UDP
IPv4 地址 + SOCK_STREAM/SOCK_DGRAM 傳輸 => OS 可自動推導出 傳輸協(xié)議(只能是) TCP/UDP
=>
// TCP 套接字
int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// UDP 套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
可將 protocol 的值設為 0, 讓 OS 自動推導出 傳輸協(xié)議
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0); //創(chuàng)建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0); //創(chuàng)建UDP套接字
chapter5 bind() / connect()
bind()
server: 綁定 套接字與 server IPAddr/Port
int bind(int sockfd, struct sockaddr *addr, socklen_t addrlen);
addrlen 可用 sizeof() 求
定義為 sockaddr_in 型, bind() 中 強轉為 sockaddr 型
sockaddr 是 sockaddr_in(IPv4) 的1層 封裝, 以 同時支持 sockaddr_in6(Ipv6)
第1字段相同
其余部分 是 char[14]
// Note: 指定 本端(Server) 協(xié)議族 / port / ipaddr
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr) );
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(1234);
// [2] bind 套接字 和 IPAddr/Port
bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr) );
in_addr_t 定義在頭文件 <netinet/in.h>, 等價于 unsigned long,長 4 Byte
s_addr 是整數(shù), 而 用戶輸入的 IPAddr 是字符串, 用要 inet_addr() 轉換
struct sockaddr_in
{
sa_family_t sin_family; // Address Family
uint16_t sin_port; // 16位 Port = 2 Byte
struct in_addr sin_addr; // 32位 IPAddr = 4 Byte
char sin_zero[8]; // 不用, 一般填 0 = 8 Byte
};
struct in_addr{
in_addr_t s_addr; // 32位 IPAddr
};
sockaddr_in
——————————————
| sin_family |
| |
| uint16_t | in_addr_t
| | —— —— —— ——
| sin_addr | - - -> | s_addr |
| | —— —— —— ——
| sin_zero[8] |
| |
——————————————
Port: uint16_t 長 2Byte, 取值范圍 0~65536,
但 0~1023 端口一般由系統(tǒng)分配給特定的服務程序
Web 服務 Port 80
FTP 服務 Port 21
client 程序 要盡量用 Port 1024~65536
struct sockaddr
{
sa_family_t sin_family;
char sa_data[14]; // Port + IPAddr + 填充
};
connect(): 與 bind() 原型相同
區(qū)別: bind()/connect() 是 server/client 端 用來 綁定/連接到 本端(server)/對端(server) 的
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);
chapter6 listen() accept()
server 端
(1) bind() 綁定 套接字
(2) listen() 讓 套接字 進入 (被動)監(jiān)聽狀態(tài)
(3) accept() 可 隨時響應 client 端的 請求 了
listen()
int listen(int sockfd, int connQueMaxSize)
sockfd: 想進入 監(jiān)聽狀態(tài)的 `普通套接字`
connQueMaxSize: connnection queue 最大 size
被動監(jiān)聽 態(tài): 沒 client 端 請求時, 套接字處于 sleep 狀態(tài), 直到收到 client 端請求, 套接字才被 "喚醒" 來 Respond
Note: listen() 讓套接字進入監(jiān)聽狀態(tài), 不阻塞; accept() 阻塞, until 有新 Request 到來
請求隊列: Request Queue / connnection queue
server 端 套接字 正處理某個 client 的 Request 時,
又收到 other clients 的 Requests, 放 Request Queue/buffer
待 `當前 Request` 處理完畢后, 再從 Request Queue 取出處理
accept()
int accept(int listenfd, struct sockaddr *clientAddr, socklen_t *pClientAddrLen);
listenfd 是 server 端 套接字
clientAddr 保存 client IPAddr/Port
accept() 返回 已連接套接字 connfd 來和 client 端 通信
chapter7 socket 數(shù)據(jù)的 發(fā)送和接收
Linux下 socket 數(shù)據(jù)的 發(fā)送和接收
Linux 中, 一切都是文件
所有 文件(如 socket ) 都可用 read()/write() 讀/寫數(shù)據(jù)
2臺計算機間 1次 socket 通信, 實際上是 server/client 對 1個 socket 文件 的 1次 寫/讀
chapter8 TCP 3次握手 & 4次揮手
1 3次握手 / 4次揮手 本質
C與S 兩端 都 進行一次 發(fā) SYN/FIN + 收 ACK, 來 確保 自己發(fā)的 SYN/FIN 被對方收到
=> 都是 4 個 分節(jié): 1 / 2 / 3 / 4
區(qū)別
1] 連接建立 第2/3 分節(jié) 合并 為1個分節(jié) 都由 對端發(fā)送 => 3次 握手
2] 連接終止 第2/3 分節(jié) 不能合并 為1個分節(jié) => 4次 揮手
原因: dataRecvQueue 還有 data, FIN 還沒取到
2 SYN / FIN & ACK 分節(jié)序號
SYN 與 FIN 均 占 1 Byte 序號空間 =>
1) SYN / FIN 序號 x: 本端本次想要發(fā)的序號
2) ACK 序號 y : `期待接收` 的 `對端下一(次) 序號` = x+1
