C++基于TCP和UDP的socket通信
阅读原文时间:2021年04月20日阅读:1

     TCP和UDP属于传输层协议。其中TCP提供IP环境下的数据可靠传输,它事先为要发送的数据开辟好连接通道(三次握手),然后再进行数据发送;而UDP则不为IP提供可靠性,一般用于实时的视频流传输,像rtp、rtsp就是建立在udp的基础上的。

     首先谈谈tcp socket

    tcp简单的三次握手过程如图,

    SYN(Synchronize Sequence Numbers):同步标志  

    ACK(Acknowledgement Number)        :确认标志

    图中可以看出,三次握手的过程是在c的connect()和s的bind()、listen()、accept()函数中完成的,这样开辟了相对可靠的连接通道,来传输数据。

UDP的socket编程过程如下图所示:

下面翠花上代码啦!

服务端:

#include <stdio.h>
#include <Winsock2.h> //windows socket的头文件

#pragma comment( lib, "ws2_32.lib" )// 链接Winsock2.h的静态库文件

void main()
{
    //初始化winsocket
    WORD wVersionRequested;
    WSADATA wsaData;
    int err;

    wVersionRequested = MAKEWORD( 1, 1 );//第一个参数为低位字节;第二个参数为高位字节

    err = WSAStartup( wVersionRequested, &wsaData );//对winsock DLL(动态链接库文件)进行初始化,协商Winsock的版本支持,并分配必要的资源。
    if ( err != 0 )
    {
        return;
    }

    if ( LOBYTE( wsaData.wVersion ) != 1 ||HIBYTE( wsaData.wVersion ) != 1 )//LOBYTE()取得16进制数最低位;HIBYTE()取得16进制数最高(最左边)那个字节的内容      
    {
        WSACleanup( );
        return;
    }

    SOCKET sockSrv=socket(AF_INET,SOCK_STREAM,0);//创建socket。AF_INET表示在Internet中通信;SOCK_STREAM表示socket是流套接字,对应tcp;0指定网络协议为TCP/IP

    SOCKADDR_IN addrSrv; 
    addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY); //htonl用来将主机字节顺序转换为网络字节顺序(to network long)
    //INADDR_ANY就是指定地址为0.0.0.0的地址,
    //表示不确定地址,或“任意地址”。”
    addrSrv.sin_family=AF_INET; 
    addrSrv.sin_port=htons(4000);//htons用来将主机字节顺序转换为网络字节顺序(to network short)

    bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));//将本地地址绑定到所创建的socket上,以使在网络上标识该socket

    listen(sockSrv,5);//socket监听,准备接受连接请求。

    SOCKADDR_IN addrClient;
    int len=sizeof(SOCKADDR);

    while(1)
    {
        SOCKET sockConn=accept(sockSrv,(SOCKADDR*)&addrClient,&len);//为一个连接请求提供服务。addrClient包含了发出连接请求的客户机IP地址信息;返回的新socket描述服务器与该客户机的连接

        char sendBuf[50];
        sprintf(sendBuf,"Welcome %s to here!",inet_ntoa(addrClient.sin_addr));//inet_ntoa网络地址转换转点分十进制的字符串指针
        send(sockConn,sendBuf,strlen(sendBuf)+1,0);

        char recvBuf[50];
        recv(sockConn,recvBuf,50,0);
        printf("%s\n",recvBuf);

        closesocket(sockConn);
        Sleep(2000);//2000毫秒
    }
    WSACleanup();
}

客户端:

#include <stdio.h>
#include <Winsock2.h>

#pragma comment( lib, "ws2_32.lib" ) 


void main()
{
    WORD wVersionRequested;
    WSADATA wsaData;
    int err;

    wVersionRequested = MAKEWORD( 1, 1 );//第一个参数为低位字节;第二个参数为高位字节

    err = WSAStartup( wVersionRequested, &wsaData );//对winsock DLL(动态链接库文件)进行初始化,协商Winsock的版本支持,并分配必要的资源。
    if ( err != 0 )
    {
        return;
    }

    if ( LOBYTE( wsaData.wVersion ) != 1 ||HIBYTE( wsaData.wVersion ) != 1 )//LOBYTE()取得16进制数最低位;HIBYTE()取得16进制数最高(最左边)那个字节的内容      
    {
        WSACleanup( );
        return;
    }
    for(int index=0;;index++)
    {
        SOCKET sockClient=socket(AF_INET,SOCK_STREAM,0);

        SOCKADDR_IN addrClt;//需要包含服务端IP信息
        addrClt.sin_addr.S_un.S_addr=inet_addr("192.168.0.30");// inet_addr将IP地址从点数格式转换成网络字节格式整型。
        addrClt.sin_family=AF_INET; 
        addrClt.sin_port=htons(4000);

        connect(sockClient,(SOCKADDR*)&addrClt,sizeof(SOCKADDR));//客户机向服务器发出连接请求
        char recvBuf[50];
        recv(sockClient,recvBuf,50,0);
        printf("my reply is : %s\n",recvBuf);

        char sendBuf[50];
        sprintf(sendBuf,"%3d,",index);
        strcat(sendBuf,"server node of: yaopeng");
        send(sockClient,sendBuf,strlen(sendBuf)+1,0);

        closesocket(sockClient);
        Sleep(2000);
    }
    WSACleanup();
}

 

对于tcp socket,有几点需要注意:

一、TCP的TIME_WAIT状态(等待客户端的相应)    

注*  TIME_WAIT 状态最大保持时间是2 * MSL,也就是1-4分钟(MSL是最大分段生存期,指明TCP报文在Internet上最长生存时间)

    当服务器端socket绑定本地地址并占用了端口,此时如果匆忙结束;或者连接的服务器异常退出,这个时候被占用的端口不能马上释放,需要TIME_WAIT。即便调用closesocket()一般也不会立即关闭socket,仍可继续重用该socket。所以重新启动服务器时可能会出现问题。例如MFC中在子窗口中实现socket通信,那么关闭子窗口再打开就会出问题了。

     解决方法是在bind()之前添加setsockopt()函数,解除端口绑定。

介绍setsockopt()之前我们再来回顾一下三次握手协议的具体流程:

第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认; 
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态; 
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

完成三次握手,客户端与服务器开始传送数据。

setsockopt()使用方法如下:

    1. 如果在已经处于 ESTABLISHED状态下的socket(一般由端口号和标志符区分)调用closesocket(一般不会立即关闭而经历TIME_WAIT的过程)后想继续重用该socket:
BOOL bReuseaddr=TRUE;
setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,(const char*)&bReuseaddr,sizeof(BOOL));

    2. 如果要已经处于连接状态的soket在调用closesocket后强制关闭,不经历TIME_WAIT的过程:
BOOL  bDontLinger = FALSE;
setsockopt(s,SOL_SOCKET,SO_DONTLINGER,(const char*)&bDontLinger,sizeof(BOOL));

更多setsockopt()函数用例可参考百度百科:http://baike.baidu.com/view/569217.htm

 二、对于大型文件,一般需要将其剁碎了一部分一部分的传。TCP不能保证接收方顺序的收到包,对于需要实时显示的文件可以在发送方发出包后设置来自接收方的响应,即对方收到前一个包后再发送下一个包。

目前就这么多,各位看官有其他的注意事项拜托请留言补充,小弟感激啊。

下面简单说下UDP socket

UDP不能保证双方的可靠连接,容易出现丢包现象。

    UDP的socket编程过程如下图所示:

上代码了,哈哈。

服务端:

#include <stdio.h>
#include <Winsock2.h>

#pragma comment( lib, "ws2_32.lib" ) 

void main()
{
    WORD wVersionRequested;
    WSADATA wsaData;
    int err;

    wVersionRequested = MAKEWORD( 1, 1 ); 

    err = WSAStartup( wVersionRequested, &wsaData ); 
    if ( err != 0 ) {
        return;
    }

    if ( LOBYTE( wsaData.wVersion ) != 1 ||
        HIBYTE( wsaData.wVersion ) != 1 ) { 
            WSACleanup( );
            return;
    }
    SOCKET sockSrv=socket(AF_INET,SOCK_DGRAM,0);

    int len=sizeof(SOCKADDR);

    SOCKADDR_IN from;   
    SOCKADDR_IN local; 
    local.sin_addr.S_un.S_addr=htonl(INADDR_ANY); 
    local.sin_family=AF_INET; 
    local.sin_port=htons(27015); 

    int a = bind(sockSrv,(SOCKADDR*)&local,len);



    while(1)
    {
        char recvBuf[50];
        recvfrom(sockSrv,recvBuf,50,0,(SOCKADDR*)&from,&len);//from收到客户端的IP信息
        printf("%s\n",recvBuf);
        printf("%s\n",inet_ntoa(local.sin_addr));
        char sendBuf[50];
        sprintf(sendBuf,"Welcome %s to here!",inet_ntoa(from.sin_addr));  
        sendto(sockSrv,sendBuf,strlen(sendBuf)+1,0,(SOCKADDR*)&from,len);

        Sleep(2000);
    }
    closesocket(sockSrv);
    WSACleanup();
}

客户端:

#include <stdio.h>
#include <Winsock2.h>

#pragma comment( lib, "ws2_32.lib" ) 


void main()
{
    WORD wVersionRequested;
    WSADATA wsaData;
    int err;

    wVersionRequested = MAKEWORD( 1, 1 ); 

    err = WSAStartup( wVersionRequested, &wsaData ); 
    if ( err != 0 ) {
        return;
    }

    if ( LOBYTE( wsaData.wVersion ) != 1 ||
        HIBYTE( wsaData.wVersion ) != 1 ) { 
            WSACleanup( );
            return;
    }


    for(int index=0;;index++)
    {
        SOCKET sockClient=socket(AF_INET,SOCK_DGRAM,0);

        int len = sizeof(SOCKADDR);

         SOCKADDR_IN local;
        local.sin_addr.S_un.S_addr=inet_addr("192.168.0.30"); 
        local.sin_family=AF_INET; 
        local.sin_port=htons(27015); 

        char sendBuf[30];
        sprintf(sendBuf,"%3d,",index);
        strcat(sendBuf,"server node of: yaopeng");
        sendto(sockClient,sendBuf,strlen(sendBuf)+1,0,(SOCKADDR*)&local,len);

        char recvBuf[50];
        recvfrom(sockClient,recvBuf,50,0,(SOCKADDR*)&local,&len);
        printf("my reply is : %s\n",recvBuf);
        printf("%s\n",inet_ntoa(local.sin_addr));

        closesocket(sockClient);
        Sleep(2000);
        WSACleanup();
    }
}

   完了。关于socket编程还有很多函数没有涉及,急待跟进完善。欢迎大家给我留言!