zoukankan      html  css  js  c++  java
  • 通信编程:Winsock socket 编程步骤与样例

    套接字编写流程

    以 TCP 套接字为例,由于 TCP 是面向连接的协议,所以基于 TCP 的套接字也需要有多个步骤。

    套接字的创建

    在进行网络通信之前,都需要使用 socket() 函数创建一个套接字对象。

    SOCKET
    WSAAPI
    socket(
        _In_ int af,
        _In_ int type,
        _In_ int protocol
        );
    
    参数 说明
    af socket 使用的地址格式
    type 指定套接字的类型
    protocol 指定使用的协议类型

    其中 WinSock 中只支持 AF_INET 作为地址格式,一般 type 确定后 protocol 也会随之确定。socket 的 type 可以是以下几种类型:

    type 类型 说明
    SOCK STREAM 流套接字 使用TCP提供有连接的可靠的传输
    SOCK DGRAM 数据报套接字 使用UDP提供无连接的不可靠的传输
    SOCK RAW 原始套接字 不使用某种特定的协议去封装它,而是由程序自行处理数据报以及协议首部

    绑定 socket 和地址

    创建了 socket 对象后,需要为该对象绑定 IP 地址和端口号,需要使用到 bind() 函数

    bind(
        _In_ SOCKET s,
        _In_reads_bytes_(namelen) const struct sockaddr FAR * name,
        _In_ int namelen
        );
    
    参数 说明
    s 套接字句柄
    name 要关联的本地地址
    namelen 地址长度

    一般来说 s 就是刚刚创建的 socket 对象,name 可一个 sockaddr_in 结构,namelen 则直接对一个 sockaddr_in 结构用 sizeof() 运算即可。

    进入监听状态

    绑定地址后,socket 就可以进入监听状态,这个时候就可以接收传来的链接信息了。为了进入监听状态,需要使用 listen() 函数

    listen(
        _In_ SOCKET s,
        _In_ int backlog
        );
    
    参数 说明
    s 套接字句柄
    backlog 监听队列的长度

    接收连接请求

    客户端想要与服务器建立一条 TCP 连接,需要使用 connect() 函数

    int
    WSAAPI
    connect(
        _In_ SOCKET s,
        _In_reads_bytes_(namelen) const struct sockaddr FAR * name,
        _In_ int namelen
        );
    

    服务器使用 accept() 函数将在监听队列中,取出未处理连接中的第一个连接,然后为这个连接创建新的套接字,返回它的句柄。新创建的套接字是处理实际连接的套接字,它与 s 有相同的属性。

    accept(
        _In_ SOCKET s,
        _Out_writes_bytes_opt_(*addrlen) struct sockaddr FAR * addr,
        _Inout_opt_ int FAR * addrlen
        );
    

    connect() 函数和 accept() 函数的参数相同:

    参数 说明
    s 套接字句柄
    name 要连接的设备的地址信息
    namelen 地址长度

    name 中的地址用来寻址远程的 socket,一般来说监听状态下是一个循环等待的过程。此时程序默认工作在阻塞模式下,如果没有未处理的连接存在,accept() 函数会一直等待下去,直到有新的连接发生才返回。

    收发数据

    对于流套接字来说,一般使用 send() 函数来发送缓冲区内的数据,返回发送数据的实际字节数。

    send(
        _In_ SOCKET s,
        _In_reads_bytes_(len) const char FAR * buf,
        _In_ int len,
        _In_ int flags
        );
    
    参数 说明
    s 套接字句柄
    buf 要发送的数据
    len 要发送的数据的长度
    flags 调用方式,通常为 0

    可以使用 recv() 函数从对方接收数据,并将其存储到指定的缓冲区。

    recv(
        _In_ SOCKET s,
        _Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf,
        _In_ int len,
        _In_ int flags
        );
    
    参数 说明
    s 套接字句柄
    buf 接收的数据要存储的变量
    len 能接收的数据的长度
    flags 调用方式,通常为 0

    在阻塞模式下,send 将会阻塞线程的执行直到所有的数据发送完毕(或者发生错误),而 recv 函数将返回尽可能多的当前可用信息,直到达到缓冲区指定的大小。

    关闭套接字

    当不使用 socket 创建的套接字时,应该调用 closesocket() 函数将它关闭。

    int
    WSAAPI
    closesocket(
        _In_ SOCKET s
        );
    
    参数 说明
    s 套接字句柄

    TCP 套接字样例

    功能设计

    模拟实现 TCP 协议通信过程,要求编程实现服务器端与客户端之间双向数据传递。也就是在一条 TCP 连接中,客户端和服务器相互发送一条数据即可。

    程序工作流程

    由于前面的流程是对于单个客户端或服务器的编码流程,这里给出一组客户端和服务器工作的流程。

    编码实现

    注意无论是客户端还是服务器,都需要包含头文件 initsock.h 来载入 Winsock。

    服务器

    #include "initsock.h"
    #include <iostream>
    using namespace std;
    
    CInitSock initSock;     // 初始化Winsock库
    
    int main()
    {
        // 创建套接字
        SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (sListen == INVALID_SOCKET)
        {
            cout << "Failed socket()" << endl;
            return 0;
        }
    
        // 填充sockaddr_in结构
        sockaddr_in sin;
        sin.sin_family = AF_INET;
        sin.sin_port = htons(4567);
        sin.sin_addr.S_un.S_addr = INADDR_ANY;
    
        // 绑定这个套接字到一个本地地址
        if (::bind(sListen, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
        {
            cout << "Failed bind()" << endl;
            return 0;
        }
    
        // 进入监听模式
        if (::listen(sListen, 2) == SOCKET_ERROR)
        {
            cout << "Failed listen()" << endl;
            return 0;
        }
    
        // 循环接受客户的连接请求
        sockaddr_in remoteAddr;
        int nAddrLen = sizeof(remoteAddr);
        SOCKET sClient;
        char szText[] = "你好!";
        while (TRUE)
        {
            cout << "服务端已启动,正在监听!
    " << endl;
    
            // 接受一个新连接
            sClient = ::accept(sListen, (SOCKADDR*)&remoteAddr, &nAddrLen);
            if (sClient == INVALID_SOCKET)
            {
                cout << "Failed accept()" << endl;
                continue;
            }
    
            cout << "与主机 " << inet_ntoa(remoteAddr.sin_addr) << "建立连接:" << endl;
    
            // 接收数据
            char buff[256];
            int nRecv = ::recv(sClient, buff, 256, 0);
            if (nRecv > 0)
            {
                buff[nRecv] = '';
                cout << "接收到数据:" << buff << endl;
            }
    
            // 向客户端发送数据
            ::send(sClient, szText, strlen(szText), 0);
            // 关闭同客户端的连接
            ::closesocket(sClient);
        }
    
        // 关闭监听套接字
        ::closesocket(sListen);
    
        return 0;
    }
    

    客户端

    #include "initsock.h"
    #include <iostream>
    using namespace std;
    
    CInitSock initSock;     // 初始化Winsock库
    
    int main()
    {
        // 创建套接字
        SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (s == INVALID_SOCKET)
        {
            cout << " Failed socket()" << endl;
            return 0;
        }
    
        // 也可以在这里调用bind函数绑定一个本地地址,否则系统将会自动安排
        // 填写远程地址信息
        sockaddr_in servAddr;
        servAddr.sin_family = AF_INET;
        servAddr.sin_port = htons(4567);
        // 填写服务器程序(TCPServer程序)所在机器的IP地址
        char serverAddr[] = "127.0.0.1";
        servAddr.sin_addr.S_un.S_addr = inet_addr(serverAddr);
    
        //与服务器建立连接
        if (::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
        {
            cout << " Failed connect()" << endl;
            return 0; 
        }
    
        cout << "与服务器 " << serverAddr << "建立连接" << endl;
    
        //向服务器发送数据
        char szText[] = "你好,服务器!";
        int slen = send(s, szText, 100, 0);
        if (slen > 0)
        {
            cout << "向服务器发送数据:" << szText << endl;
        } 
    
        // 接收数据
        char buff[256];
        int nRecv = ::recv(s, buff, 256, 0);
        if (nRecv > 0)
        {
            buff[nRecv] = '';
            cout << "接收到数据:" << buff << endl;
        }
    
        // 关闭套接字
        ::closesocket(s);
        return 0;
    }
    

    运行效果

    UDP 套接字

    工作流程

    UDP 用户数据报协议提供的是无连接的、不可靠的数据传输服务,但是不需要像 TCP 那样建立连接,所以传输效率高。

    数据收发

    UDP 使用的发送数据的函数是 sendto(),比较不同的是发送数据时需要填写接收方的地址。

    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(),接收数据时需要把发送方的地址存出来。

    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
        );
    

    sendto() 和 recvfrom() 的参数相似:

    参数 说明
    s 用来发送数据的套接字
    buf 指向发送数据的缓冲区
    len 要发送数据的长度
    flags 一般指定为 0
    to/from 指向一个包含目标地址和端口号的 sockaddr in 结构
    tolen/fromlen sockaddr in 结构的大小

    程序编写

    服务器

    #include "initsock.h"
    #include <iostream>
    using namespace std;
    CInitSock initSock;     // 初始化Winsock库
    
    int main()
    {
        // 创建套接字
        SOCKET s = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
        if (s == INVALID_SOCKET)
        {
            cout << "Failed socket()" << endl;
            return 0;
        }
    
        // 填充sockaddr_in结构
        sockaddr_in sin;
        sin.sin_family = AF_INET;
        sin.sin_port = htons(4567);
        sin.sin_addr.S_un.S_addr = INADDR_ANY;
    
        // 绑定这个套接字到一个本地地址
        if (::bind(s, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
        {
            cout << "Failed bind()" << endl;
            return 0;
        }
    
        cout << "服务端已启动!
    " << endl;
    
        // 进入循环
        char buff[1024];
        sockaddr_in addr;
        int nLen = sizeof(addr);
        while (TRUE)
        {
            //接收数据
            int nRecv = ::recvfrom(s, buff, 1024, 0, (sockaddr*)&addr, &nLen);
            if (nRecv > 0)
            {
                buff[nRecv] = '';
                cout << " 接收到" << ::inet_ntoa(addr.sin_addr) << "的数据:" << buff << endl;
            }
    
            // 发送数据
            char szText[] = "你好,客户端!";
            ::sendto(s, szText, strlen(szText), 0, (sockaddr*)&addr, sizeof(addr));
        }
        ::closesocket(s);
    }
    

    客户端

    #include "initsock.h"
    #include <iostream>
    using namespace std;
    
    CInitSock initSock;     // 初始化Winsock库
    
    int main()
    {
        // 创建套接字
        SOCKET s = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
        if (s == INVALID_SOCKET)
        {
            printf("Failed socket() %d 
    ", ::WSAGetLastError());
            return 0;
        }
    
        // 也可以在这里调用bind函数绑定一个本地地址
        // 否则系统将会自动安排
    
        // 填写远程地址信息
        sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(4567);
        //填写服务器程序所在机器的IP地址
        addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    
        // 发送数据
        char szText[] = "你好,服务器!";
        ::sendto(s, szText, strlen(szText), 0, (sockaddr*)&addr, sizeof(addr));
        
        char buff[1024];
        sockaddr_in client_addr;
        int nLen = sizeof(client_addr);
        int nRecv = ::recvfrom(s, buff, 1024, 0, (sockaddr*)&client_addr, &nLen);
        if (nRecv > 0)
        {
            buff[nRecv] = '';
            cout << "  接收到" << ::inet_ntoa(client_addr.sin_addr) << "的数据:" << buff << endl;
        }
    
        ::closesocket(s);
        return 0;
    }
    

    运行效果

    参考资料

    《Windows 网络与通信编程》,陈香凝 王烨阳 陈婷婷 张铮 编著,人民邮电出版社

  • 相关阅读:
    java中如何高效的判断数组中是否包含某个元素---
    反射--
    Json----
    Ajax学习(1)
    Jdbc学习---
    java---内部类
    java中的多态
    spring是什么
    quartz的配置文件
    浅谈Job&JobDetail
  • 原文地址:https://www.cnblogs.com/linfangnan/p/15377421.html
Copyright © 2011-2022 走看看