网络编程-001-windows-socket

socket 概念

  • socket 中文译名套接字, 是网络通信的基本操作单元, 可以看做是不同主机之间进程进行双向通信的端点, 即通信双方的一种约定, 可用socket的相关函数来完成通信过程
  • socket是网络通信双方之间的纽带, 应用程序在网络上发送, 接受的信息都通过socket实现
  • socket可建立一次连接, 并对连接唯一标识
  • socket是操作系统的资源

socket 网络编程规范

  • Berkeley socket
    最早出现在BSD Unix中, 形成一套API
  • Windows socket(Winsock)
    通过动态链接库的形式加入Windows操作系统
    增加异步函数, 支持Windows消息驱动机制的网络事件异步选择

Windows socket 网络编程规范

Windows socket 1.1

  • 库函数语法, 符号常量, 数据结构在 winsock.h
  • 静态编译时用 wsock32.lib
  • 动态调用时用 winsock.dll
  • 继承 Berkeley socket规范
  • 扩充 Berkeley socket规范, 增加了 WSA 开头的函数
  • 只支持 TCP/IP 协议

windows socket 2.2

  • 库函数语法, 符号常量, 数据机构在 WinSock2.h
  • 静态编译时用 ws2_32.lib
  • 动态调用时用 ws2_32.dll
  • 支持多种协议
  • 支持服务质量 QoS
  • 增加异步机制和重叠I/O
  • 更多的函数

socket 的类型

  • stream socket (字节流套接字)
    当要使用TCP协议通信时, 可使用此类型的套接字, 可提供面向连接的, 无差错的, 发送先后顺序一致的, 非重复的网络数据传输
  • datagram socket (数据报套接字)
    当要使用UDP协议通信时, 可使用此类型套接字, 可提供无连接的服务, 相互独立的数据传输
  • raw socket (原始套接字)
    提供对网络下层通信协议(如IP协议)的直接访问, 可用于开发新协议或特定网络功能

socket 网络编程思路

  1. 创建socket
  2. 将socket与地址结构绑定
  3. 发送/接受数据
  4. 释放socket

Windows socket 网络编程思路

  1. 初始化 Windows socket
  2. 创建socket
  3. 将socket与地址结构绑定
  4. 发送/接收数据
  5. 释放socket
  6. 终止Windows socket

socket常用函数和数据结构

  • windows socket 的初始化和终止
    • WSAStartup(
    • WSACleanup(
  • 创建和释放socket(
    • socket(
    • closesocket(
  • 绑定socket和地址结构
    • bind(
    • listen(
    • accept(
    • connetc(
    • sockaddr
    • sockaddr_in
    • in_addr
  • 发送, 接收数据
    • send(
    • sendto(
    • recv(
    • recvfrom(
  • 错误处理函数
    • WSAGetLastError(
  • 其他辅助函数
    • htons(
    • htonl(
    • ntohs(
    • ntohl(
    • inet_addr(
    • inet_ntoa(
    • gethostbyname(
    • gethostbyaddr(
    • getservbyname(

初始化函数 WSAStartup(

  • 用Winsock编写的网络应用程序要做的第一件事, 调用 WSAStartup( 完成初始化
  • 只有初始化成功后, 才能调用其他 socket API
  • 函数定义

    int
    WSAAPI
    WSAStartup(
        _IN_ WORD wVersionRequested, 
        // WinSock版本号高位字节指定副版本号, 低位字节指定主版本号,
        //可用宏MAKEWORD(X, Y)进行设置MAKEWORD(2, 2)
        _Out_ LPWSADATA lpWSAData
        // 返回一个WSADATA结构
    
        );
    // 返回值
    // 成功: 返回0
    // 失败: 返回错误代码
    // WSASYSNOTREADY (网络系统未准备好)
    // WSAVERNOTSUPPORTED (版本不支持)
    // WSAEINPROGRESS (正在执行一个阻塞的操作)
    // WSAEFAULT (指针lpWSAData不合法)
  • WSAStartup( 的功能
    • 当被调用时, 找到系统目录, 根据环境变量PATH, 查找WinSock.dll
    • 检查 WinSock.dll 版本号, 符号要求, 则调用成功
    • 绑定应用程序与 WinSock.dll
    • 函数成功返回时, 在 lpWSAData 指向的 WSADATA 结构中返回信息
  • WSAData 结构保存 WSAStartup( 返回的初始化信息
  • WSAData 结构体定义

    typedef struct WSAData {
            WORD                    wVersion;
            WORD                    wHighVersion;
    
    #ifdef _WIN64
    
            unsigned short          iMaxSockets;
            unsigned short          iMaxUdpDg;
            char FAR *              lpVendorInfo;
            char                    szDescription[WSADESCRIPTION_LEN+1];
            char                    szSystemStatus[WSASYS_STATUS_LEN+1];
    
    #else
    
            char                    szDescription[WSADESCRIPTION_LEN+1];
            char                    szSystemStatus[WSASYS_STATUS_LEN+1];
            unsigned short          iMaxSockets;
            unsigned short          iMaxUdpDg;
            char FAR *              lpVendorInfo;
    
    #endif
    
    } WSADATA, FAR * LPWSADATA;
  • 注销函数WSACleanup(

    • 当网络应用程序使用完WinSock.dll, 使用 WSACleanup( 接触绑定, 终止对WinSock.dll的调用,释放引用动态链接库时占用的系统资源
    • 函数定义

      int
      WSAAPI
      WSACleanup(
      void
      );
    • 函数返回值
    • 成功: 返回 0
    • 失败 返回 SOCKET_ERROR
  • 在所有 Windows Sockets的应用程序里, WSAStartup( 和 WSACleanup(是必须使用的, 且成对使用

socket( 创建函数

  • 定义

    SOCKET
    WSAAPI
    socket(
    _In_ int af,
    _In_ int type,
    _In_ int protocol
    );
    • af (address family 地址族)

      常量 类型
      AF_UNIX Unix域socket
      AF_INET Internet TCP/IP
      AF_IPX Novell IPX
      AF_IRDA 红外线通信
    • type

      常量 socket 类型
      SOCK_STREAM 字节流(有连接的)
      SOCK_DGRAM 数据报(无连接的)
      SOCK_RAW 原始套接字
    • protocol 表示特定的协议, 一般取 0
    • 返回值
      • 成功: 返回新 socket 的描述符 (无符号整数)
      • 失败: 返回 INVALID_SOCKET
  • 函数功能
    根据指定的通信地址族, 套接字类型和通信协议创建一个新的套接字, 为它分配资源, 并返回该套接字的描述符
  • 例子

    /*创建一个数据流 socket */
    SOCKET socket1 = socket(AF_INET, SOCK_STREAM, 0);
    /*创建一个数据报 socket */
    SOCKET socket2 = socket(AF_INET, SOCK_DGRAM, 0);

closesocket( 关闭函数

  • 函数的功能
    关闭一个 socket, 释放 socket 描述符, 再访问时返回 WSAENOTSOCK错误
  • 定义

    int
    WSAAPI
    closesocket(
    _In_ SOCKET s
    );
  • 返回值
    • 成功: 返回 0
    • 失败: 返回 SOCKET_ERROR

关闭 socket

  • 关闭socket会关闭用户应用程序中的socket句柄, 释放相关资源, 同时会隐含的触发TCP连接的关闭过程
  • TCP连接的关闭过程
    • 优雅关闭 (graceful close)
      如果发送缓存中还有数据未发出则其发出去, 并且收到所有数据的ACK之后, 发送FIN包, 开始关闭过程
    • 强制关闭 (hard close或abortive close)
      如果缓存中还有数据, 则这些数据都将被丢弃, 然后发送RST包, 直接重置TCP连接
  • SO_LINGER 选项的设置值决定了 closesocket( 的行为, 该选项的参数值是linger结构

    struct  linger {
    u_short l_onoff;                /* option on/off */
    u_short l_linger;               /* linger time */
    };
  • l_onoff==0, 优雅关闭, socket的资源会被保留直到TCP连接关闭
  • l_onoff > 0 && l_linger == 0, 强制关闭
  • l_onoff > 0 && l_linger > 0, 则在l_linger表明的时间内关闭

bind( 绑定函数

  • 功能
    • 将 socket 绑定到指定的网络地址上, 一般在 connect( 或listen( 函数前调用
    • 在服务器端, 用作监听客户端连接请求的 socket 一定要经过绑定
    • 在客户端使用的套接字一般不必绑定, 除非要指定它使用特定的网络地址
  • 定义

    int
    WSAAPI
    bind(
    _In_ SOCKET s,
    _In_reads_bytes_(namelen) const struct sockaddr FAR * name,
    _In_ int namelen
    );
  • 返回值
    • 成功: 返回 0
    • 失败: 返回 SOCKET_ERROR
  • sockaddr 结构体

    • 功能
      表示通用的 WinSock 地址结构, 针对各种通信域的套接字, 存储它们的地址信息
    • 定义

      typedef struct sockaddr {
      
      #if (_WIN32_WINNT < 0x0600)
      
          u_short sa_family;
      
      #else
      
          ADDRESS_FAMILY sa_family;           // Address family.
      
      #endif //(_WIN32_WINNT < 0x0600)
      
          CHAR sa_data[14];                   // Up to 14 bytes of direct address.
      } SOCKADDR, *PSOCKADDR, FAR *LPSOCKADDR;
      • sa_family
        地址族: 与socket( 的第一个参数相同含义
      • sa_data
        协议地址, 内容因具体协议而不同
  • sockaddr_in 结构体

    typedef struct sockaddr_in {
    
    #if(_WIN32_WINNT < 0x0600)
    
        short   sin_family;
    
    #else //(_WIN32_WINNT < 0x0600)
    
        ADDRESS_FAMILY sin_family;
    
    #endif //(_WIN32_WINNT < 0x0600)
    
        USHORT sin_port;
        IN_ADDR sin_addr;
        CHAR sin_zero[8];
    } SOCKADDR_IN, *PSOCKADDR_IN;
    • sin_family 必须为 AF_INET
    • sin_port 端口号
    • sin_addr IPv4 地址
    • sin_zero[8] 对齐
  • in_addr 结构体, 存储 IP地址

    typedef struct in_addr {
       union {
           struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
           struct { USHORT s_w1,s_w2; } S_un_w;
           ULONG S_addr;
       } S_un;
    
    #define s_addr  S_un.S_addr /* can be used for most tcp & ip code */
    
    
    #define s_host  S_un.S_un_b.s_b2    // host on imp
    
    
    #define s_net   S_un.S_un_b.s_b1    // network
    
    
    #define s_imp   S_un.S_un_w.s_w2    // imp
    
    
    #define s_impno S_un.S_un_b.s_b4    // imp #
    
    
    #define s_lh    S_un.S_un_b.s_b3    // logical host
    
    } IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;
  • 三种地址结构的关系
    三种地址结构的关系

listen( 监听函数

  • 功能
    • 适用于支持连接的 socket
    • 在Internet通信域, 仅用于字节流 socket, 并仅用于服务器端。
    • 监听 socket 必须已绑定在特定的网络地址上
    • 监听 socket 能监听来自客户端的连接请求, 并规定了等待连接队列 (先进先出队列) 的最大长度
  • 定义

    int
    WSAAPI
    listen(
    _In_ SOCKET s,
    _In_ int backlog
    );
  • 返回值
    • 成功: 返回0
    • 失败: 返回 SOCKET_ERROR

accept( 接收连接请求

  • 功能
    • 从监听 socket 的等待队列中抽取连接请求, 创建一个新的 socket 来与请求连接的客户端 socket 创建连接通道, 交换数据
    • 如果连接成功, 返回新创建的 socket 描述符
    • 若队列中没有连接请求, 当:
      • 阻塞方式时, 该函数阻塞调用它的进程。
      • 非阻塞方式时, 该函数返回一个错误代码
  • 定义

    SOCKET
    WSAAPI
    accept(
    _In_ SOCKET s,
    _Out_writes_bytes_opt_(*addrlen) struct sockaddr FAR * addr,
    _Inout_opt_ int FAR * addrlen
    );
    • s 服务端的监听 socket
    • addr 客户端地址结构
    • addrlen 客户端地址长度
    • 返回值
      • 成功: 新的 socket 描述符
      • 失败: INVALID_SOCKET错误

connect( 请求连接函数

  • 功能
    客户端请求与服务器端建立连接
  • 定义

    SOCKET
    WSAAPI
    accept(
    _In_ SOCKET s,
    _Out_writes_bytes_opt_(*addrlen) struct sockaddr FAR * addr,
    _Inout_opt_ int FAR * addrlen
    );
    • s socket 若未绑定, 操作系统赋值绑定
    • addr 服务器端的地址结构
    • addrlen 服务器端地址长度
    • 返回值
      • 成功: 返回 0
      • 失败: 返回 SOCKET_ERROR

send( 发送数据函数

  • 功能
    在已建立连接的 socket 上发送数据
  • 定义

    int
    WSAAPI
    send(
    _In_ SOCKET s,
    _In_reads_bytes_(len) const char FAR * buf,
    _In_ int len,
    _In_ int flags
    );    
    • s 已连接的 socket
    • buf 待发送缓冲区
    • len 待发送内容长度
    • flags 选项, 一般为0
    • 返回值
      • 成功: 返回实际发送的字节数
      • 失败: 返回SOCKET_ERROR
  • 注意
    • 真正向对方发送数据的过程是由协议栈完成的
    • 数据报类型的套接字, 发送数据的长度不应超过子网的IP包最大长度, (最大长度在WSADATA结构中定义)
    • 成功完成send( 调用不代表数据能传送到对方

recv( 接收数据函数

  • 功能
    在已建立连接的 socket 上接收数据
  • 定义

    int
    WSAAPI
    recv(
    _In_ SOCKET s,
    _Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf,
    _In_ int len,
    _In_ int flags
    );
    • s 已连接的 socket
    • buf 接受缓冲区
    • len 缓冲区长度
    • flags 选项, 一般为 0
    • 返回值
      • 成功: 返回实际接收的字节数
      • 连接已终止: 0
      • 失败: 返回 SOCKET_ERROR

sendto( 数据报 socket 发送数据函数

  • 功能
    用发送端的 socket 发送一个数据报, 数据发送由协议栈完成
  • 定义

    int
    WSAAPI
    sendto(
    _In_ SOCKET s,
    _In_reads_bytes_(len) const char FAR * buf,
    _In_ int len,
    _In_ int flags,
    _In_reads_bytes_(tolen) const struct sockaddr FAR * to,
    _In_ int tolen
    );

recvfrom( 数据报 socket 接收数据函数

  • 定义

    int
    WSAAPI
    recvfrom(
    _In_ SOCKET s,
    _Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf,
    _In_ int len,
    _In_ int flags,
    _Out_writes_bytes_to_opt_(*fromlen, *fromlen) struct sockaddr FAR * from,
    _Inout_opt_ int FAR * fromlen
    );

WSAGetLastError( 错误捕获函数

  • 调用任何一个 WinSock 函数之后, 可以用WSAGetLastError函数来获得详细的错误代码
  • 定义

    int
    WSAAPI
    WSAGetLastError(
    void
    );
    • 返回值
      错误代码
      • WSANOTINITIALISED: 使用本API函数之前,未成功调用WSAStartup(
      • WSAENETDOWN: 网络子系统失效
      • WSAEADDRINUSE: 指定端口已被占用
      • WSAEFAULT: name和namelen参数定义的地址结构非法
  • 注意: WSAStartup( 调用失败不能用 WSAGetLastError( 获得错误代码。是因为还未建立存储错误信息的空间

字节序

  • Big-Endian (大端) 是指低位字节排放在内存的高端, 高位字节排放在内存的低端 而Little-Endian (小端) 正好相反
  • Big-Endian, Little-Endian跟多字节类型的数据有关, 比如 int, short, long型, 对byte型却没有影响
  • 举例:
    int a = 0x05060708
    在Big-Endian的情况下存放为:
    地址增长方向—>0x00|0x01|0x02|0x03
    数据存储0x05|0x06|0x07|0x08
    在Little-Endian的情况下存放为:
    地址增长方向—> 0x00|0x01|0x02|0x03
    数据存储0x08|0x07|0x06|0x05
  • 主机字节顺序
    Big-Endian, Little-Endian跟CPU有关, Intel架构采用Little-Endian, 而PowerPC, SPARC和Motorola架构采用Big-Endian
  • 网络字节顺序
    指数据在网络上传输时是大端先发送还是小端先发送, Internet通信域的网络字节顺序是Big-Endian, 即无论计算机系统支持何种Endian, 在传输数据是,总是数值最高位的字节最先发送

字节序转换函数

  • u_short htons( u_short hostshort )
  • u_long htonl( u_long hostlong )
  • u_short ntohs( u_short netshort )
  • u_long ntohl( u_long netlong )
  • 功能
    将16bit和32bit的数据在主机字节顺序和网络字节顺序之间相互转换
    在基于socket的网络应用编程中, 一般需要变换IP地址和端口号

地址转换函数

  • unsigned long inet_addr( const char* cp )
    将ASCII字符串地址转换成32位网络字节顺序的IP地址
  • char* FAR inet_ntoa( struct in_addr in )
    将32位网络字节顺序的IP地址转换成ASCII字符串地址

网络信息查询函数

  • struct hostent FAR * gethostbyname( const char FAR * name )
  • struct hostent FAR * gethostbyaddr( const char FAR * addr, int len, int type )
  • struct servent FAR * getservbyname( const char FAR * name, const char FAR * proto )
struct  hostent {
        char    FAR * h_name;           /* official name of host */
        char    FAR * FAR * h_aliases;  /* alias list */
        short   h_addrtype;             /* host address type */
        short   h_length;               /* length of address */
        char    FAR * FAR * h_addr_list; /* list of addresses */
#define h_addr  h_addr_list[0]          /* address, for backward compat */
};

struct  servent {
        char    FAR * s_name;           /* official service name */
        char    FAR * FAR * s_aliases;  /* alias list */
#ifdef _WIN64
        char    FAR * s_proto;          /* protocol to use */
        short   s_port;                 /* port # */
#else
        short   s_port;                 /* port # */
        char    FAR * s_proto;          /* protocol to use */
#endif
};

示例约定

使用 Microsoft Visual Studio Community 2017
去掉 stdafx.h 和 stdafx.cpp, 不使用 c++
由于VS编码问题, 只使用 ASCII 字符
由于会用到一些老旧的函数, 在文件开头定义常量

#define _WINS
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值