Winsock编程接口

这个是基于Winsock2的一个程序设计,我使用的软件是VS2017,当然其他软件应该也可以编译运行demo,这篇博客主要写一些Winsock接口问题,主要包含winsock库的装入和释放,winsock的寻址方式和字节顺序(大端小端),winsock编程流程,典型流程图,TCP的服务端与客户端编程,UDP的服务端与客户端编程以及时间协议和网络对时demo。

所有源代码可以在我的主页里面寻找对应的压缩包下载。


    
    Winsock 是一个真正的协议无关的接口

2.1 Winsock库 现在使用Winsock2 WS2_32.lib 库链接
    2.1.1 Winsock库的装入和释放
      

 int WSAStartup(
            WORD wVersionRequested,        //指定想要加载的winsock库版本,高字节是次版本号,低字节是主版本号。
            LPWSADATA lpWSAData            //指向WSADATA结构体的 指针 ,返回DLL库的详细信息。
            );
        wVersionRequested参数使用 宏 MAKEWORD(x,y) x是高字节,y是低字节。
        LPWSADATA 是一个结构体,WSAStartup使用所加载库的版本信息填充。
        
        typedef struct WSAData
        {
            WORD wVersion;                                //库文件建议应用程序使用的版本
            WORD wHighVersion;                            //库文件支持的最高版本
            char szDescription[WSADESCRIPTION_LEN+1]    //库描述字符串
            char szSystemStatus[WSASYS_STATUS_LEN+1]    //系统状态字符串
            unsigned short iMacSoskets;                    //同时支持最大套接字数量
        }WSADATA,FAR*LPWSADATA;


        
        WSAStartup 成功返回0,否则用 WSAGetLastErroe()获取错误信息,它是调用API函数里的GetLastError,获取的是最后发生错误的代码。
        
        **每一个对WSAStartup的调用,必须对应一个WSACleanup释放Winsock库
            int WSACleanup(void);
        **链接WS2_32.lib库 用 #pragma comment(lib,"WS2_32");
-----------------------------------------------------------------------------
2.2 Winsock的寻址方式和字节顺序
        winsock兼容多个协议,所以必须使用通用的寻址方式。TCP/IP使用IP地址和端口号指定一个地址,但是其他协议也许采用不通的协议。
      

  struct sockaddr
        {
            u_short sa_family;    //地址使用的地址家族。
            char sa_data[14];    //存储的数据在不同的地址家族中可能不同。
        };


        在Winsock中,通过 SOCKADDR_IN 结构体指定IP和端口:
        

struct sockaddr_in
        {
            short sin_family;             //地址家族(指定地址格式),因为AF_INET;
            u_short sin_port;            //端口号
            struct in_addr sin_addr;    //IP地址
            char sin_zero[8];            //空字节,设置为0;
        };


        sin_port:指定了TCP/UDP通信服务端口号(16位)。端口号分为3个范围:
            0~1023 : 由IANA管理,保留为公共的服务使用。 20/21 80 
            1024~49151: 普通用户注册的端口号,由IANA列出。  
            49152~65535: 是动态或者私有的算口号。
        
        sin_addr域用来存储IP地址(32位),被定义一个联合来处理整个32位的值,两个16位部分单独分开。
        

struct in_addr{
            union{
                struct {u_char s_b1,s_b2,s_b3,s_b4} S_un_b;  //'aa.bb.cc.dd',每个不能超过255
                struct {u_short s_w1,s_w2} S_un_w;
                u_long S——addr;
            }S_un;
        };


        
        sin_zero:没有使用,是为了与SOCKADDR结构体大小相同蔡设置。
        **可以用inet_addr函数将IP的点分十进制转换为二进制表示。
        **inet_ntoa 是 inet_addr 的逆函数。
        unsigned long inet_addr(const char *cp);
        char *inet_ntoa(struct in_addr in);
        
        字节顺序:
            长度跨越多个字节的数据被存储的顺序。如:0x12345678,小端存储:0x78,0x56,0x34,0x12    ==> 不重要的字节首先存储。
            大端顺序(大尾顺序):最重要的字节首先存储。
        因为协议数据要在这些机器间传输,所以必须 选定其中一种方式作为标准,否则容易引起混淆。
        TCP/IP统一使用大端传输,也称为 网络字节序。
        在sockaddr_in中除了sin_family 外,其余的都采用网络字节序存储
            u_short htons(u_short hostshort);
            u_short ntohs();
            u_long htonl();
            u_long ntohl();
----------------------------------------------------------------------------------------
获取地址信息:
    通常,主机上的接口被静态地指定一个IP地址, 169.254.0.0/16  范围内的地址
获取Mac地址:获取自己和LAN中的MAC地址
    获取本机的MAC地址很容易,用函数 GetAdaptersInfo 即可。
  

 DWORD GetAdaptersInfo(
        PIP_ADAPTER_INFO pAdapterInfo,        //指向一个缓冲区,来获取IP_ADAPTER_INFO结构的列表
        PULONG pOutBufLen                    //指定缓存区的大小,如果大小不够,此参数返回大小
    );                                      //函数调用成功返回ERROR_SUCCESS;

    IP_ADAPTER_INFO 结构包含了本地计算机上网络适配器的信息。
  

 typedef struct _IP_ADAPTER_INFO{
        struct _IP_ADAPTER_INFO *Next;    //指向适配器列表中的下一个适配器(计算机可能有多个适配器)
        DWORD comboIndex;                //保留字段
        char AdapterName[MAX_ADAPTER_NAME_LENGTH+4];    //适配器名称
        char Description[MAX_ADAPTER_DESCRIPTION_LENGTH+4];    //对适配器的描述
    
        UINT AddressLength;        //MAC地址的长度 48位
        BYTE Address[MAX_ADAPTER_ADDRESS_LENGTH];    //mac地址
        DWORD Index;    //适配器索引
        UINT Type;    //适配器类型,如MIB_IF_TYPE_ETHERNET等
        UINT DhcpEnabled;    //指定适配器是否使DHCP(动态主机配置)有效
        PIP_ADDR_STRING CurrentIpAddress;    //保留字段
        IP_ADDR_STRING IpAddressList;    //此适配器相关的IP列表
        IP_ADDR_STRING GatewayList;        //网关地址列表
        IP_ADDR_STRING DhcpServer;        //HDCP服务器
        BOOL HaveWins;    //指定此适配器是否使用WINS
        IP_ADDR_STRING PrimaryWinsServer;    //WINS服务器的主IP地址
        IP_ADDR_STRING SecondaryWinsServer;    //WINS服务器的第二IP地址
        time_t LeaseObtained;    //获取当前DHCP租用时间
        time_t LeaseExpires;    //当前DHCP租用期满的时间

    }IP_ADAPTER_INFO,*PIP_ADAPTER_INFO;


--------------------------------------------------------------------------
2.3 流程:Winsock编程的一般步骤是比较固定的。
    不管是TCP还是UDP,都先初始化sock;

class CinitSock
{
    public:
        CinitSock(BYTE minorVer = 2,BYTE majorVer = 2)
        {
            WSADATA wsaData;
            WORD sockVersion = MAKEWORD(minorVer,majorVer);
            if (::WSAStartup(sockVersion, &wsaData) != 0)
            {
                ::WSAGetLastError();
            }
        }
        ~CinitSock()
        {
            ::WSACleanup();
        }
 };

TCP:
    1.套接字的创建和关闭
        

SOCKET socket(
            int af,                //指定套接字的使用的地址格式,winsock只支持AF_INIT
            int type,            //用来指定套接字类型
            int protocol        //配合type参数使用,用来指定使用的协议类型。可以是IPPROTO_TCP/UDP等,也可以是0
        );


        type参数用来指定套接字的类型。有流,数据报,原始三类。
        SOCK_STREAM:    流套接字,使用TCP提供有连接的可靠传输。
        SOCK_DGRAM:        数据报套接字,使用UDP提供无连接的不可靠传输。
        SOCK_RAW:        原始套接字,Winsock接口不使用特定的协议去封装,由程序自行处理数据报以及协议首部。
        失败返回-1(INVALID_SOCKET)

        当不使用套接字时,应用closesocket关闭,成功返回0,失败返回SOCKET_ERROR;
      

 int closesocket(SOCKET sockfd);


    2.绑定指定的IP和端口号:绑定的是本地的
      

 int bind(
            SOCKET s,    //
            const struct sockaddr FAR * name,    //
            int namelen    //
        );


        **inet_pton(AF_INET, "127.0.0.1", &add.sin_addr);和 inet_addr()功能一样。但VS2017要加
                #include <WS2tcpip.h> 和 #define _WINSOCK_DEPRECATED_NO_WARNINGS
    3.设置套接字进入监听  仅在支持连接的套接字上使用
        int listen(SOCKET sockfd,int backlog);
        backlog:监听队列中允许保持的尚未处理的最大连接数量。
        如果连接数量已满,客户端将收到WSAAECONNREFUSED的错误。
    4.接受连接请求
      

 SOCKET accept(
            SOCKET s,        //服务端的套接字
            const struct sockaddr* name,    //一个指向sockaddr_in的指针,用于获取对方的地址信息
            int *addrlen    //指向地址长度的指针
        );


        该函数在s上取出未处理连接中的第一个连接,然后为这个连接创建新的套接字,返回它的句柄,新创建的套接字是处理实际连接的套接字,它与s有相同的属性
        默认是阻塞模式,会一直accept下去,直到有新的连接发生才返回。
        addrlen用于指定addr所指空间的大小,也返回实际地址的长度,如过addr或addrlen是NULL,则没有关于远程地址的信息返回。

        客户端在创建套接字成功后,要使用connect函数请求连接
      

  int connect(
         SOCKET s,    //
         const struct sockaddr FAR * name,    //服务器的地址信息
         int namelen //sockaddr_in 的长度
         );


    5.收发数据        默认是阻塞
        send()    直到数据发送完毕或出错才返回
        recv()    尽可能多的返回当前可用信息,直到达到缓冲区指定的大小

*************************************************************************************************
*                            初始化Winsock                                                                                *
*            Server                                    Client                                                                 *
*            socket()                                socket()    //不需要绑定,系统会自动安排。       *
*            bind()                                    connect()                                                             *
*            listen()                                                                                                              *
*            accept()        recv()/send()                                                                                *
*                            closesocket()                                                                                    *
*                            释放Winsock                                                                                    *
*************************************************************************************************

UDP
    UDP用的是sendto 和 recvfrom 
    

int sendto(
        SOCKET s,
        const char FAR * buf,            //发送数据缓冲区
        int len,                        //发送的长度
        int flags,                        //一般指定为0
        const struct sockaddr FAR * to,    //目的地址的sockaddr_in
        int tolen                        //sockaddr_in的大小
    );
    int recvfrom(
        SOCKET s,
        char FAR * buf,                //接收数据缓冲区
        int len,                    //接收长度
        int flags,                    //0
        struct sockaddr FAR * from,    //返回发送方的sockaddr_in
        int FAR * fromlen            //sockadddr_in的长度
    );

    注意:创建套接字之后,如果先调用sockto,则可以不用bind()显式的绑定本地地址,系统会自动绑定,所以后面调用recvfrom()也不会报错。
        如果先调用recvfrom(),就会出错,因为没有绑定本机地址。

---------------------------------------------------------------------------------------------------
            TCP                    UDP

Server            socket()
        bind()
        listen()
        recv()/send            recvfrom()/sendto()
                     closesocket()

Client
                      socket()
      recv()/send            recvfrom()/sendto()
                    closesocket()
----------------------------------------------------------------------------------------------------
2.4 网络对时程序
    时间协议[Time Protocol](RFC-868)是一种引用层协议。它返回一个为格式化的32位二进制数字,这个数字描述了从1900年1月1日0时0分0秒到现在的秒数,
    以TCP/UDP的格式返回相应。
    将服务器时间转换为本地时间是客户端的责任(借用文件时间)。
    工作过程:
        S : 监听37端口
        C : 连接到37端口
        S : 以32位二进制(unsigned int)数据发送时间。
        C : 接收时间
        C : 关闭连接
        S : 关闭连接
        如果服务器不能决定用什么时间,服务器会拒绝连接或不发生任何数据直接关闭连接。

        
        
        这是学习这一部分所编写的demo
        

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值