未分类 · 2023年3月26日 0

快速理解 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 
#include 
#include 
#include 
#include 
#include 
#include 

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 
#include 
#include 
#include 
#include 
#include 
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: %sn", 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 
#include 

#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 
#include 
#include 
#include 
#include 
#include 

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: %sn", 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

     

    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 定义在头文件 , 等价于 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
观察分组
打赏 赞(0) 分享'
分享到...
微信
支付宝
微信二维码图片

微信扫描二维码打赏

支付宝二维码图片

支付宝扫描二维码打赏

文章目录