Windows网络编程(基础篇1)

Windows网络编程(基础篇1)

  1. Winsock是一种网络编程接口,不是协议。
  2. 除了WSAStartup、WSACleanup、WSARecvEx、WSAGetLastError属于Winsocket1.1规范函数外,凡是有前缀WSA的,都是在Winsock 2 中更新或者增添的一个新的API函数。

一、Winsock初始化

  1. 包含头文件winsock2.h,链接库WS2_32

    include 
    
    #pragma comment(lib,"WS2_32")
    
  2. 使用Winsock的应用都必须加载合适的Winsock DLL版本,否则返回SOCKET_ERROR。使用WSAStartup加载,最后需要调用WSACleanup释放Winsock分配的资源。

    int WSAStartup(
     _In_  WORD      wVersionRequested,
     _Out_ LPWSADATA lpWSAData
    );
    • wVersionRequested:版本号,高阶字节指定小版本号,低位字节指定主版本。
    • lpWSAData 指向WSADATA数据结构的,接收Windows Sockets实现细节。
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);//成功返回0

    MAKEWORD:创建一个无符号16位整形,通过连接两个给定的无符号参数,也就是将(2,2)放入wVersionRequested中。

    WSAGetLastError();//返回调用winsock函数发生的错误代码
    WSACleanup();//程序结束时,需要调用释放资源

二、SOCKADDR_IN简介

  1. SOCKADDR_IN:用来指定IP地址和端口信息。

    typedef struct sockaddr_in {
       short   sin_family;          //The address family for the transport address,must AF_INET
       USHORT sin_port;         //port number
       IN_ADDR sin_addr;            // IPv4 transport address
       CHAR sin_zero[8];            //Reserved(预留) for system use
    } SOCKADDR_IN, *PSOCKADDR_IN;
  2. inet_pton 转换字符串到网络地址。将“点分十进制” -> “二进制整数”(inet_addr已弃用)

    //m_HostGroup.sin_addr.s_addr = inet_addr(strGroupIP);//代替方法如下:

    inet_pton(AF_INET, strGroupIP, (void*)&m_HostGroup.sin_addr.s_addr);

    INT WSAAPI InetPton(
     _In_  INT     Family,          // AF_INET and AF_INET6.
     _In_  PCTSTR pszAddrString,    //待转换的地址,IPV4 或 IPV6
     _Out_ PVOID  pAddrBuf          //转换后的(IPV4:IN_ADDR,IPV6: IN6_ADDR
    );
  3. htons 将整型变量从主机字节顺序转变成网络字节顺序

    u_short WSAAPI htons(_In_ u_short hostshort);
  4. ntohl 将网络字节顺序转换成主机字节顺序

    u_long WSAAPI ntohl(_In_ u_long netlong);
    • 创建SOCKADDR_IN结构示例:
    SOCKADDR_IN sin;
    WORD Port=80;
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(Port);
    inet_pton(AF_INET, "192.168.1.1", (void*)&sin.sin_addr.S_un.S_addr);

三、套接字通信(TCP)

  1. 创建套接字函数:socket、WSASocket

    SOCKET WSAAPI socket(
     _In_ int af,           //指定协议族 AF_INET、AF_INET6、AF_LOCAL等
     _In_ int type,     //指定Socket类型 (TCP)SOCK_STREAM、(UDP)SOCK_DGRAM、SOCK_RAW等
     _In_ int protocol      //指定协议 IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP等
    );
  2. 创建套接字后,就必须将套接字绑定到一个已知地址上:bind

    int bind(
     _In_ SOCKET                s,          //待连接的套接字
     _In_ const struct sockaddr *name,      //地址缓冲区(sockaddr *)sockaddr_in
     _In_ int                   namelen //name大小
    );
  3. 将套接字置入监听模式:listen;(bind只是将套接字和指定地址关联,listen指示套接字等候连接)

    int listen(
     _In_ SOCKET s,         //待监听的套接字
     _In_ int    backlog        //等待连接队列的最大长度
    );
  4. 有客户端连接到达时,接收一个连接:accept

    SOCKET accept(
     _In_    SOCKET          s,     //正在监听的套接字
     _Out_   struct sockaddr *addr, //连接者的地址
     _Inout_ int             *addrlen   //指向存有addr地址长度的整数
    );
  5. 客户端通过套接字连接到服务端:connect

    int connect(
     _In_ SOCKET                s,  
     _In_ const struct sockaddr *name,
     _In_ int                   namelen
    );
  6. 服务端示例:

    
    #include<winsock2.h>
    
    
    #pragma comment(lib,"WS2_32")
    
    int main(void)
    {
    WSADATA wsaData;
    if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        return 0;
    }
    SOCKET ListeningSocket;
    SOCKET NewConnetction;
    SOCKADDR_IN ServerAddr;
    SOCKADDR_IN ClientAddr;
    int Port = 5150;
    
    //创建一个套接字来监听客户端连接
    ListeningSocket = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    ServerAddr.sin_family = AF_INET;
    ServerAddr.sin_port = htons(Port);
    ServerAddr.sin_addr.S_un.S_addr = INADDR_ANY;
    
    //用bind将套接字信息和地址信息绑定
    ::bind(ListeningSocket, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr));
    
    //监听客户连接,限制5个
    ::listen(ListeningSocket, 5);
    
    //连接到达时,接受一个新连接
    int ClientaddrLen;
    NewConnetction = ::accept(ListeningSocket, (SOCKADDR*)&ClientAddr, &ClientaddrLen);
    
    //此时在这些套接字上可以做:
    //1.在ListeningSocket上再次调用accept,等待更多的连接。2.在NewConnection上完成数据收发。
    
    //关闭套接字
    closesocket(NewConnetction);
    closesocket(ListeningSocket);
    
    WSACleanup();
    return 1;
    }
  7. 客户端示例:

    
    #include
    
    
    #include
    
    
    #pragma comment(lib,"WS2_32")
    
    int main(void)
    {
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        return 0;
    }
    SOCKET S;
    SOCKADDR_IN ServerAddr;
    int Port = 5150;
    
    //创建一个套接字来建立客户端连接
    S = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    ServerAddr.sin_family = AF_INET;
    ServerAddr.sin_port = htons(Port);
    inet_pton(AF_INET, "192.168.1.1", (void*)&ServerAddr.sin_addr.S_un.S_addr);
    connect(S, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr));
    
    //关闭套接字
    closesocket(S);
    WSACleanup();
    return 1;
    }
  8. 为什么客户端不用bind

    • 无连接的socket的客户端和服务端以及面向连接socket的服务端通过调用bind函数来配置本地信息。使用bind函数时,通过将my_addr.sin_port置为0,函数会自动为你选择一个未占用的端口来使用。
    • 有连接的socket客户端通过调用Connect函数在socket数据结构中保存本地和远端信息,无须调用bind(),因为这种情况下只需知道目的机器的IP地址,而客户通过哪个端口与服务器建立连接并不需要关心,socket执行体为你的程序自动选择一个未被占用的端口,并通知你的程序数据什么时候打开端口。
    • 服务端进程bind IP地址:目的是限制了服务端进程创建的socket只接受那些目的地为此IP地址的客户链接

    1.需要在建连前就知道端口的话,需要 bind
    2.需要通过指定的端口来通讯的话,需要 bind

四:数据传输

  1. 要在已经建立的套接字上发送数据,可以用send和WSASend。接收数据可以用recv和WSARecv。

    int send(
     _In_       SOCKET s,       //是一个已经建立了连接,用于发送数据的套接字
     _In_ const char   *buf,    //指向即将发送数据的缓冲区
     _In_       int    len, //缓冲区内的字符数
     _In_       int    flags    //调用执行方式
    );//如果成功,返回的是发送的字节数,否则返回SOCKET_ERROR
    int WSASend(
     _In_  SOCKET                             s,
     _In_  LPWSABUF                           lpBuffers,
     _In_  DWORD                              dwBufferCount,
     _Out_ LPDWORD                            lpNumberOfBytesSent,
     _In_  DWORD                              dwFlags,
     _In_  LPWSAOVERLAPPED                    lpOverlapped,
     _In_  LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
    );

    char sendbuff[2048];int nBytes=2048; sendbuff=******;

    ret=send(s,sendbuff,nBytes,0);

    对于send函数而言,可能返回已发出的字节数少于给定的字节数。因为对于每个收发数据的套接字来说,系统都为他们分配了相当充足的缓冲区空间,所以返回值ret变量将被设为已发送的字节数。在发送数据时,内部缓冲区都会将数据一致保留到可以将它发到线上为之。比如,传输大量的数据可以领缓冲区快速填满。同时,对TCP/IP来说,还有一个窗口大小的问题。接收端会对窗口大小进行调节,以指示它可以接收多少数据。如果有大量数据涌入接收端,接收端就会将窗口大小设为0,为挂起数据做好准备。对发送端来说,这样会强制它在收到一个新的大于0的窗口大小之前,不得再发送数据。在使用send调用时,缓冲区可能只能容纳1024字节,这时,便有必要重新提交剩下的1024字节。

    char sendbuff[2018]; int nBytes=2048, nLeft, idx;
    nLeft=nBytes;    idx=0;
    while(nLeft>0)
     {
       ret=send(s,&sendbuff[idx],nLeft,0);
       if(ret==SOCKET_ERROR)
         {
           //error
         }
       nLeft-=ret;
       idx+=ret;
     }

  2. 在已经建立连接的套接字上接受数据的传入,可以使用recv和WSARecv。

    int recv(
     _In_  SOCKET s,
     _Out_ char   *buf,
     _In_  int    len,
     _In_  int    flags
    );
    int WSARecv(
     _In_    SOCKET                             s,
     _Inout_ LPWSABUF                           lpBuffers,
     _In_    DWORD                              dwBufferCount,
     _Out_   LPDWORD                            lpNumberOfBytesRecvd,
     _Inout_ LPDWORD                            lpFlags,
     _In_    LPWSAOVERLAPPED                    lpOverlapped,
     _In_    LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
    );
    

    流套接字是一个不间断的数据流,在读取它时,应用程序通常不会关心应该读多少数据。如果所有消息长度都一样,则处理要简单,比如读取512字节。

    char recvbuff[1024]; int ret, nLeft, idx;
    nLeft=512;
    idx=0;
    while(nLeft>0)
     {
       ret=recv(s,&recvbuff[idx],nleft,0);
       if(ret==SOCKET_ERROR)
         {
           //error
         }
        idx+=ret;
       nLeft-=ret;
     }

    如果消息长度不同,就必须要利用自己的协议来通知接收端,让它知道即将到来的消息长度是多少。比如,写入接收端的前4个字节总是整数,用来标记即将到来的消息长度。

  3. 中断连接

    一旦完成了套接字连接,就必须将它关掉,并释放关联到那个套接字句柄的所有资源,执行closesocket即可。但是,closesocket可能带来的负面影响就是导致数据丢失。鉴于此,在调用closesocket函数之前,利用shutdown函数从容中止连接。

    int shutdown(
     _In_ SOCKET s,
     _In_ int    how    //SD_RECEIVE,SD_SEND, SD_BOTH
    );//关闭一个套接字

    对closesocket调用释放的套接字描述符,如果再次利用该套接字就会调用失败。如果没有对改套接字的其他引用,那么所有与套接字描述符相关的资源都会被释放,包括丢弃所有队列中的数据。

你可能感兴趣的:(C++,网络编程)