参考
http://c.biancheng.net/cpp/socket/
本教程不要求读者有Linux和Windows开发经验, 也不需要深入了解 TCP/IP 协议, 涉及到相关知识时都会说明
同时学 Linux和Windows 的原因
大多项目在 Windows/Linux 下开发 client/server 端, 单独学1种平台 没实践意义
两大平台下 socket 编程非常相似
网络编程 就是 编写程序 使两台联网的计算机 相互交换数据
, 这就是 socket 全部内容
吗?是的!
socket 编程 远比想象中简单
chapter1 socket 简介
socket: 套接字, 计算机间通信的一种 约定
socket 典型应用: Web 服务器 和 浏览器
浏览器
获取 用户输入的 URL
向 服务器 发起请求
服务器
分析收到的 URL
将对应的网页内容返回给浏览器
浏览器
解析和渲染, 将文字、图片、视频 呈现给用户
1 IP Address
(1) 封装
到要发送的数据包
(2) 被 路由器
用于 寻址
: 据 IP Address 找到 dst 计算机
本机地址: 127.0.0.1
2 Port
(1) 用于 区分
不同的 网络程序
网络程序 端口号
Web 服务 80
FTP 服务 21
SMTP 服务 25
(2) 是 虚拟/逻辑 概念
可视为 一道门
, data 通过这道门 流入流出, 每道门有不同的 编号
, 即 端口号
3 Protocol
网络通信的双方遵守的约定
TCP/IP 协议族: TCP IP UDP Telnet FTP SMTP 等上百个关联协议
TCP IP 常用
4 数据传输方式
常用2种:
(1) SOCK_STREAM
面向连接
重发
http 协议用
(2) SOCK_DGRAM
无连接
不作数据校验
错了不重发
QQ 视频/语音聊天 用
总结
IP Address 和 Port
能在互联网中 定位到要通信的程序
Protocol 和 数据传输方式
规定了 如何传输 数据
有了这些, 两台计算机就可以通信了
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 协议
int listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Note: 指定 本端(Server) 协议族 / 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] 监听: 将 `普通套接字` 转化为 `监听套接字`
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] 发 数据 给 client: 向 套接字文件 写 数据
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) 协议族 / 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 传回数据后, 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 传回数据后, 程序运行结束
(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 下 主要区别:
(1) Windows 下 socket 程序依赖 动态链接库 Winsock.dll 或 ws2_32.dll,必须提前加载
(2) Linux 用 "文件描述符", Windows 用 "文件句柄"
Linux 不区分 socket 文件和普通文件, 而 Windows 区分
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) 协议族 / 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 传回数据后, 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() 读/写数据
创建的文件都有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 数据传输方式
SOCK_STREAM / SOCK_DGRAM
protocol 传输协议
IPPROTO_TCP / IPPTOTO_UDP
IPv4 地址 + SOCK_STREAM/SOCK_DGRAM 传输 => OS 可自动推导出 传输协议(只能是) 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 自动推导出 传输协议
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0); //创建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) 协议族 / 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 是整数, 而 用户输入的 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 端口一般由系统分配给特定的服务程序
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() 原型相同
区别: 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() 让 套接字
进入 (被动)监听状态
(3) accept() 可 随时响应
client 端的 请求
了
listen()
int listen(int sockfd, int connQueMaxSize)
sockfd: 想进入 监听状态的 `普通套接字`
connQueMaxSize: connnection queue 最大 size
被动监听 态:
没 client 端 请求时, 套接字处于 sleep 状态
, 直到收到 client 端请求, 套接字才被 "唤醒" 来 Respond
Note: listen() 让套接字进入监听状态, 不阻塞; 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 数据的 发送和接收
Linux下 socket 数据的 发送和接收
Linux 中, 一切都是文件
所有 文件(如 socket ) 都可用 read()/write() 读/写数据
2台计算机间 1次 socket 通信
, 实际上是 server/client 对 1个 socket 文件 的 1次 写/读
chapter8 TCP 3次握手 & 4次挥手
1 3次握手 / 4次挥手 本质
C与S 两端 都 进行一次 发 SYN/FIN + 收 ACK
, 来 确保 自己发的 SYN/FIN 被对方收到
=> 都是 4 个 分节: 1 / 2 / 3 / 4
区别
1] 连接建立 第2/3 分节 合并 为1个分节
都由 对端发送 => 3次 握手
2] 连接终止 第2/3 分节 不能合并
为1个分节 => 4次 挥手
原因: dataRecvQueue 还有 data, FIN 还没取到
2 SYN / FIN & ACK 分节序号
SYN 与 FIN 均 占 1 Byte 序号空间
=>
1) SYN / FIN 序号 x: 本端本次想要发的序号
2) ACK 序号 y : `期待接收` 的 `对端下一(次) 序号` = x+1