Windows Socket编程基础

本文介绍Winsock编程的基础知识,包括Socket套接字的概念、Socket函数的使用方法、IP地址及字节顺序的转换技巧,以及服务器端和客户端的基本通信流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

摘要

在学习服务器的过程中需要使用到winsock以及Socket编程,这里记录一下学到的Socket基础


  1. Socket套接字

    Socket就是一种通过(IP + Port)并遵循TCP/UDP等协议的进程间通信的技术,类似于UNIX中的管道,详细的教程度娘上很多。

    Socket主要有两类:
    (1) 标准Socket,又称为Berkeley Socket,主要用于Linux/UNIX
    (2) Winsock,主要用于Win平台

  2. Socket函数

    以Winsock为例,有一系列基本的Socket函数用来实现Socket通信

    (1) Socket
    // Socket 函数用于创建一个Socket套接字,函数原型如下
    SOCKET socket(
      int af, //使用的协议族
      int type, //Socket类型
      int protocol //使用的协议地址
    );
    // Socket 协议族在计算机里表示为一个整数,可以取值AF_INET
    // Socket 类型有两种:SOCK_STREAM 和 SOCK_DGRAM,代表流Socket(Tcp)和数据报Socket(UDP)
    
    // 如果函数成功,返回一个Socket套接字,否则,返回INVALID_SOCKET,在建立Socket连接之前,必须先创建所需的Socket套接字
    SOCKET s;
    s = socket(AF_INET, SOCK_STREAM, 0);
    

    在QT使用socket函数的时候出现’undefined reference to `_imp__socket’ 错误,解决办法是在PRO文件中加入如下语句:

    CONFIG += c++11 // 以防万一
    LIBS += -lpthread libwsock32 libws2_32

    而且在头文件中需要引入<windows.h> 或者 <winsock2.h>


    让我们接着Socket往下说

    (2) Connect
    // Connect函数用于尝试与远端建立一个Socket连接,函数原型如下:
    int connect(
        SOCKET s,   //Socket描述字
        const struct sockaddr* name,  //远端的地址
        int namelen   //远端地址的长度
    );
    // 在进行连接时,远端地址是一个SOCKADDR的结构,定义为:
    struct sockaddr_in {
        short sin_family,  //Socket组
        u_short sin_port, // 端口
        struct in_addr sin_addr, //IP地址
        char sin_zero[8]  //结构的长度
    }
    // 如果连接成功,返回0,否则返回SOCKET_ERROR。
    // 对于非阻塞模式的Socket连接,返回结果通常都是SOCKET_ERROR,
    //    并且错误代码为WSAEWOULDBLOCK,表示连接正在进行,而不是一个真正的错误
    
    //建立连接通常都是由客户端发出连接请求
    SOCKET s;
    SOCKADDR_IN ServerAddr;
    ServerAddr.sin_falimy = AF_INET;
    ServerAddr.sin_port = htons(80);  //也可以自定义其他Port
    ServerAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    connect(s, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr));

    在QT使用connect函数的时候,会与QT本身的connect函数冲突,解决办法是加::域分隔符,如下:

    ::connect(s,(SOCKADDR*)&ServerAddr, sizeof(ServerAddr));

    让我们接着Connect往下说

    (3) Send
    // Send函数用于在某个Socket上向远端发送数据,原型为:
    int send(
        SOCKET s,
        const char* buf,   //存放发送数据的缓冲区
        int len,  //将要发送的数据长度
        int flags  //发送时使用的附加参数
    )
    // 如果发送成功,返回成功发送的字节数,否则返回SOCKET_ERROR
    
    (4) Recv
    int recv(
        SOCKET s,
        char* buf,  //存放接受数据的缓冲区
        int len,  //接受缓冲区的大小
        int flags  //接受时使用的附加标志
    )
    
    (5) Closesocket
    // 用于关闭不在需要的Socket
    int closesocket(
        SOCKET s  //Socket套接字
    )
    // 成功返回0,否则返回SOCKET_ERROR
    
     (6) Listen
     //Listen用于在某个Socket上建立监听,函数原型为:
     int listen(
         SOCKET s,
         int backlog   //缓存队列的长度
     )
     // 成功返回0,否则返回SOCKET_ERROR
     // 关于backlog参数,一般理解为协议层已经开始或已经完成连接的建立,但是应用程序还没有开始接受数据时,连接的数量未知。
     // 以前通常都是设为5,因为BSD4.2最大支持5,但是现在都是取SOMAXCONN,表示最大
    
     (7) Accept
     // 用于接受一条新的连接,注意:是接受连接而不是接受数据,原型为
     SOCKET accept(
         SOCKET s,  //监听中的Socket
         struct sockaddr* addr,  //表示地址结构体的指针
         int* addrlen //地址结构体的长度
     )
     // 成功返回一个新的Socket套接字,否则返回SOCKET_ERROR
    
     (8) Bind
     //用于给一个Socket套接字分配一个本地协议地址,原型为
     int bind(
         SOCKET s,
         const struct sockaddr* name,  //表示地址结构体的指针
         in namelen  //地址结构体的程度
     )
     // 成功返回0,否则返回SOCKET_ERROR
    
     (9) Select
     // 用于检测Socket状态,主要用于高级的网络通信模型:
     int select(
         int nfds, //Winsock中此函数无意义
         fd_set* readfds, //进行可读检测的Socket
         fd_set* writefds,//进行可写检测的Socket
         fd_set* exceptfds, //进行异常检测的Socket
         const struct timeval* timeout // 非阻塞模式中设置最大等待时间
     )
      // 成功返回0,否则返回SOCKET_ERROR
  3. IP地址转换

    主要有三种IP地址
    (1) 无符号整数地址:127.0.0.1
    (2) ASCII地址:“127.0.0.1”
    (3) 域名地址:“localhost”

    转换方法:

    (1) 整数地址到ASCII地址的转换
    
    #include <arpa/inet.h>
    
    // ASCII => INT
    int inet_aton(const char *straddr, struct in_addr* adrp); // 0表示失败,1表示成功
    
    // INT => ASCII
    char *inet_ntoa( struct in_addr inaddr); //NULL表示失败,其他任何值表示成功
    
    
    (2) 域名地址与整数地址的相互转换
    
    # include <netdb.h>
    
    // DOMAIN => INT
    struct hostent *gethostbyname( const char * name);
    
    // INT => DOMAIN
    struct hostent *gethostbyaddr( const char *addr, int len, int family);
  4. 字节转换

    由于大端对齐和小端对齐的原因,不同的计算机系统在进行网络通信时需要一个统一的格式,也就是Big Endian格式,称之为“网络字节顺序”(network byte order),相对的本地计算机使用的数据格式为“本机字节顺序”(host byte order)

    关于大小端对齐的问题,计算机专业的童鞋应该不会陌生,不清楚的可以看看这篇blog:
    http://blog.youkuaiyun.com/yangcs2009/article/details/39698997

    在发送到网络之前,必须先转化为网络字节顺序,系统提供了一些相应的函数,用于字节顺序的转换,这些函数命名有一定的规律:h代表字节顺序(host或理解为本机顺序);n代表网络顺序(network)

    u_long PASCAL FAR  hton1 ( IN u_long hostlong);
    //本地字节转化为网络字节顺序(长整数)
    u_short PASCAL FAR htons(IN u_short hostshort);
    //本地字节转化为网络字节顺序(短整数)
    u_long PASCAL FAR ntoh1 (IN u_long netlong);
    //网络字节转化为本地字节顺序(长整数)
    u_short PASCAL FAR ntohs ( IN u_short netshort);
    //网络字节转化为本地字节顺序(短整数)

    Q: 为什么在数据结构sockaddr_in中的sin_addr和sin_port要转换成网络字节顺序,而sin_family不需要呢?
    A: 答案是:sin_addr和sin_port分别封装在包的IP 和 UDP层,但是sin_family只是被本机使用来决定数据结构中包含什么类型的地址,没有被发送到网络上

  5. 基本Socket通信

    服务器端基本流程:[初始化监听Socket]->[接受新的客户端连接]->[收发数据]->[关闭连接]

    //!!注意:在Windows下必须包含头文件ws2tcpip.h ,否则会报错:socklen_t was not declared in this scope
    
    //!!注意:在Windows下执行socket相关操作前,必须初始化WSADATA!!!!
    WSADATA wsaData;
    int nRet;
    if((nRet = WSAStartup(MAKEWORD(2,2),&wsaData)) != 0){
        exit(0);
    }

    (一) 初始化监听Socket

    // (1) 初始化Socket
    SOCKET s;
    s = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP) // 使用流Socket,基于TCP协议
    
    // (2)绑定Socket
    SOCKADDR_IN ServerAddr;
    ServerAddr.sin_family = AF_INET;
    ServerAddr.sin_port = htons(9000);
    ServerAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    
    bind(s, (LPSOCKADDR)&ServerAddr, sizeof(ServerAddr));
    
    // (3)开始监听
    listen(s, SOMAXCONN);

    (二) 建立连接

    // (1) 检测Socket状态:函数select可以检测相应的Socket状态,从而决定是否需要建立新的连接
    fd_set readset;
    timeval timeout;
    timeout.tv_sec = 0;
    timeout.tv_usec = 0;
    FD_ZERO(&readset);
    FD_SET(s, &readset);
    int ret = select(FD_SETSIZE, &readset, NULL, NULL, &timeout);
    if(ret>0 && FD_ISSET(s, &readset))
    {
        //新连接
    }
    
    // (2) 建立新的连接:如果客户端有新的连接请求介入,则建立一个新的连接
    // 注意:使用accept建立新的连接时,需要使用新的socket进行通信(temp)
    SOCKADDR_IN ServerAddr;
    int len = sizeof(ServerAddr);
    SOCKET temp;
    temp = accept(s, (SOCKADDR*) &ServerAddr, (socklen_t*)&len);
    if(temp == INVALID_SOCKET){
        //链接失败
    }

    (三) 收发数据

    // (1) 检测读入数据
    fd_set readset;
    timeval timeout;
    timeout.tv_sec = 0;
    timeout.tv_usec = 0;
    FD_ZERO(&readset);
    FD_SET(s, &readset);
    int ret = select(FD_SETSIZE, &readset, NULL, NULL, &timeout);
    if(ret>0 && FD_ISSET(s, &readset))
    {
        //有新数据
    } 
    
    // (2) 接收数据
    char buf[1024];
    in ret;
    ret = recv(s, buf, 1024, 0);
    
    // (3) 检测发送数据
    fd_set sendset;
    timeval timeout;
    timeout.tv_sec = 0;
    timeout.tv_usec = 0;
    FD_ZERO(&sendset);
    FD_SET(s, &sendset);
    int ret = select(FD_SETSIZE, &sendset, NULL, NULL, &timeout);
    if(ret>0 && FD_ISSET(s, &sendset))
    {
        //可以发送新数据
    }  
    
    // (4) 发送数据
    char buf[1024];
    int ret;
    ret = send(s, buf, 1024, 0);

    (四) 关闭连接

    closesocket(s)

    客户端基本流程:[初始化监听Socket]->[建立连接]->[收发数据]->[关闭连接]

    (一) 初始化监听Socket

    // (1) 初始化Socket
    SOCKET s;
    s = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP) // 使用流Socket,基于TCP协议
    
    // (2)绑定端口
    SOCKADDR_IN ServerAddr;
    ServerAddr.sin_family = AF_INET;
    ServerAddr.sin_addr_s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(0);
    bind(s, (LPSOCKADDR)&ServerAddr, sizeof(ServerAddr));
    // 客户端的IP地址和端口号都不需要固定,可以由系统分配,因此可以不适用bind函数绑定端口和协议
    
    // (3) 建立连接
    SOCKADDR_IN ServerAddr;
    ServerAddr.sin_family = AF_INET;
    ServerAddr.sin_port = htons(9000);
    ServerAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    connect(s, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr));
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值