快速理解 socket 编程 (C/C++)

参考
    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
观察分组
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容

  • 网络中进程之间如何通信 为了方便大家获取源代码,可以移步这里,GitHub源代码 进程通信的概念最初来源于单机系统...
    batbattle阅读 14,159评论 1 5
  • 下面为Daytime这个服务的源代码例子,同时兼容IPV6和IPV4的地址,最后部分有更多说明。 单播模式下的Se...
    天楚锐齿阅读 5,649评论 0 2
  • 什么是socket(套接字) socket是一种计算机间约定好的传输方式(有点抽象)。其本身为一串数字,unix将...
    Cooder阅读 253评论 0 0
  • 简介 Socket理论 Socket工作流程 核心函数讲解 服务的如何获取客户端的信息 字符串ip和网络二进制的转...
    第八区阅读 3,533评论 0 4
  • 网络编程离不开socket,小猿圈这篇详解一下socket创建,仔细学完这篇对你认识网络底层的东西有着很重要的作用...
    小猿圈加加阅读 100评论 0 0