C++ socket通讯详解及注意事项

Socket是什么

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),这些函数我们在后面进行介绍。

将socket通信类比为打电话这一生活场景。这里我把TCP服务器比作政府某一服务部门能,TCP客户端比作企业中某一部门电话,描述这一过程,恰好就像是socket通信,服务部门提供服务,企业部门申请服务。
要实现通信,首先政府部门都必须申请一个电话(socket_fd),并向有关部门注册(我们的系统),提供地址(sockadrr)以及属于哪个部门的(port),录入系统后,就算是合约生效了(bind),于是乎,政府广而告之,这个服务热线就算开通了,在部门里面的人员所需要做的事情,就是等待企业家拨打热线(listen)。
企业家拨打电话对地点和部门没有这么多的要求了,他并不需要绑定地址和部门,在任何一个可以拨打电话的地方(可能是同个部门,也可以同公司不同部门,甚至可能是竞争对手),他只需要拿起一个已经注册的电话(socket_fd),拨打电话(connect)
政府部门接通电话(accept)后,桥梁就打通了(服务者client_fd、顾客server_fd),可以进行听说了(read write)。企业家咨询完成(close),政府到点下班关闭服务(close)

什么是TCP/IP、UDP

 `TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网(WANs)设计的。
     UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是属于TCP/IP协议族中的一种。
    这里有一张图,表明了这些协议的关系。` 

*   1
*   2
*   3

C++ socket通讯详解及注意事项_第1张图片

socket中TCP的三次握手建立连接

我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:

客户端向服务器发送一个SYN J
服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
客户端再想服务器发一个确认ACK K+1
只有就完了三次握手,但是这个三次握手发生在socket的那几个函数中呢?请看下图:
C++ socket通讯详解及注意事项_第2张图片
从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。

总结:客户端的connect在三次握手的第二个次返回,而服务器端的accept在三次握手的第三次返回,这也是dos攻击的基本原理。

socket中TCP的四次握手释放连接

上面介绍了socket中TCP的三次握手建立过程,及其涉及的socket函数。现在我们介绍socket中的四次握手释放连接的过程,请看下图:
C++ socket通讯详解及注意事项_第3张图片
图示过程如下:
某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
接收到这个FIN的源发送端TCP对它进行确认。
这样每个方向上都有一个FIN和ACK。

socket通信流程

socket是"打开—读/写—关闭"模式的实现,以使用TCP协议通讯的socket为例,其交互流程基本如下图所示:

C++ socket通讯详解及注意事项_第4张图片

socket的基本操作

既然socket是“open—write/read—close”模式的一种实现,那么socket就提供了这些操作对应的函数接口。下面以TCP为例,介绍几个基本的socket接口函数。

socket()函数

int socket(int domain, int type, int protocol);
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:

domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。
protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。

当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。

除了socket()函数之外还有其它bind(),accept()等函数,这些函数在下面的例子中依次介绍:

服务器端代码

`#define _CRT_SECURE_NO_WARNINGS
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include
#include
#include
#include //引用头文件
#pragma comment(lib,"ws2_32.lib") //链接库文件
using namespace std;

char Ip[20][200] = { '0' };
int iConnect = 0;                     //当前客户端数量

/*
DWORD是无符号的,相当于unsigned long ,它是MFC的数据类型。DWORD一般用于返回值不会出现负值情况
WINAPI是一个宏,所代表的符号是__stdcall, 函数名前加上这个符号表示这个函数的调用约定是标准调用约定,windows API函数采用这种调用约定
LPVOID是一个没有类型的指针,也就是说你可以将任意类型的指针赋值给LPVOID类型的变量(一般作为参数传递),然后在使用的时候再转换回来。 可以将其理解为long型的指针,指向void型
*/

DWORD WINAPI threadpro(LPVOID pParam)           //创建多线程函数,函数返回值为DWORD WINAPI
{
    SOCKET hsock = (SOCKET)pParam;              //把pParam转换为SOCKET型指针
    char buffer[1024];
    char sendBuffer[1024];
    if (hsock != INVALID_SOCKET)                INVALID_SOCKET表示无效
        cout<<"Start receive information from IP:"<< Ip[iConnect] << endl << endl;
    while (true)                                                  //循环接收发送的内容
    {

        int num = recv(hsock, buffer, 1024, 0);                   //阻塞函数,等待接受内容
        if (num <= 0)
        {
            cout <<"Client with IP:"<= 0)
            cout << "Information from:" << Ip[iConnect] << ":" << buffer << endl << endl;
        if (!strcmp(buffer, "AAA"))           //如果接受到 AAA 返回 BBB
        {
            memset(sendBuffer, 0, 1024);                      
            strcpy(sendBuffer, "BBB");
            int ires = send(hsock, sendBuffer, sizeof(sendBuffer), 0);         //回送信息
            cout << "The message sent to IP:"< iMaxConnect)               //判断连接数是否大于最大连接数 
            {
                int ires = send(m_Server[iConnect], WarnBuf, sizeof(WarnBuf), 0);
            }
            else
            {
                HANDLE m_Handle;         //线程句柄
                DWORD nThreadId = 0;     //线程ID
                nThreadId++;

/*
    当使用CreateProcess调用时,系统将创建一个进程和一个主线程。CreateThread将在主线程的基础上创建一个新线程,大致做如下步骤:
    
    1在内核对象中分配一个线程标识/句柄,可供管理,由CreateThread返回
    
    2把线程退出码置为STILL_ACTIVE,把线程挂起计数置1
    
    3分配context结构
    
    4分配两页的物理存储以准备栈,保护页设置为PAGE_READWRITE,第2页设为PAGE_GUARD
    
    5lpStartAddr和lpvThread值被放在栈顶,使它们成为传送给StartOfThread的参数
    
    6把context结构的栈指针指向栈顶(第5步)指令指针指向startOfThread函数
    GreateThread语法如下:
    hThread = CreateThread (&security_attributes, dwStackSize, ThreadProc,pParam, dwFlags, &idThread) ;
    
    第一个参数是指向SECURITY_ATTRIBUTES型态的结构的指针。在Windows 98中忽略该参数。在Windows NT中,它被设为NULL。

    第二个参数是用于新线程的初始堆栈大小,默认值为0。在任何情况下,Windows根据需要动态延长堆栈的大小。
  
    第三个参数是指向线程函数的指针。函数名称没有限制,但是必须以下列形式声明:
                                     DWORD WINAPI ThreadPro (PVOID pParam) ;
 
  CreateThread的第四个参数为传递给ThreadProc的参数。这样主线程和从属线程就可以共享数据。
  
    CreateThread的第五个参数通常为0,但当建立的线程不马上执行时为旗标CREATE_SUSPENDED。线程将暂停直到呼叫ResumeThread来恢复线程的执行为止。

    第六个参数是一个指标,指向接受执行绪ID值的变量。

*/
                m_Handle = (HANDLE)::CreateThread(NULL, 0, threadpro, (LPVOID)m_Server[--iConnect], 0, &nThreadId); //启动线程
            }    
        }
    }

/*

    WSACleanum语法如下:
    int WSACleanup(void);

*/
    WSACleanup();     //用于释放ws2_32.dll动态链接库初始化时分配的资源
}` 

*   1
*   2
*   3
*   4
*   5
*   6
*   7
*   8
*   9
*   10
*   11
*   12
*   13
*   14
*   15
*   16
*   17
*   18
*   19
*   20
*   21
*   22
*   23
*   24
*   25
*   26
*   27
*   28
*   29
*   30
*   31
*   32
*   33
*   34
*   35
*   36
*   37
*   38
*   39
*   40
*   41
*   42
*   43
*   44
*   45
*   46
*   47
*   48
*   49
*   50
*   51
*   52
*   53
*   54
*   55
*   56
*   57
*   58
*   59
*   60
*   61
*   62
*   63
*   64
*   65
*   66
*   67
*   68
*   69
*   70
*   71
*   72
*   73
*   74
*   75
*   76
*   77
*   78
*   79
*   80
*   81
*   82
*   83
*   84
*   85
*   86
*   87
*   88
*   89
*   90
*   91
*   92
*   93
*   94
*   95
*   96
*   97
*   98
*   99
*   100
*   101
*   102
*   103
*   104
*   105
*   106
*   107
*   108
*   109
*   110
*   111
*   112
*   113
*   114
*   115
*   116
*   117
*   118
*   119
*   120
*   121
*   122
*   123
*   124
*   125
*   126
*   127
*   128
*   129
*   130
*   131
*   132
*   133
*   134
*   135
*   136
*   137
*   138
*   139
*   140
*   141
*   142
*   143
*   144
*   145
*   146
*   147
*   148
*   149
*   150
*   151
*   152
*   153
*   154
*   155
*   156
*   157
*   158
*   159
*   160
*   161
*   162
*   163
*   164
*   165
*   166
*   167
*   168
*   169
*   170
*   171
*   172
*   173
*   174
*   175
*   176
*   177
*   178
*   179
*   180
*   181
*   182
*   183
*   184
*   185
*   186
*   187
*   188
*   189
*   190
*   191
*   192
*   193
*   194
*   195
*   196
*   197
*   198
*   199
*   200
*   201
*   202
*   203
*   204
*   205
*   206
*   207
*   208
*   209
*   210
*   211
*   212
*   213
*   214
*   215
*   216
*   217
*   218
*   219
*   220
*   221
*   222
*   223
*   224
*   225
*   226
*   227
*   228
*   229
*   230
*   231
*   232
*   233
*   234
*   235
*   236
*   237
*   238
*   239
*   240
*   241
*   242
*   243
*   244
*   245
*   246
*   247
*   248
*   249
*   250
*   251
*   252
*   253
*   254
*   255
*   256
*   257
*   258
*   259
*   260
*   261
*   262
*   263
*   264
*   265
*   266
*   267
*   268
*   269
*   270
*   271
*   272
*   273
*   274
*   275
*   276
*   277
*   278
*   279
*   280
*   281
*   282
*   283
*   284
*   285
*   286
*   287
*   288
*   289
*   290
*   291
*   292
*   293
*   294
*   295
*   296
*   297
*   298
*   299
*   300

客户端代码

`#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include
#include
#include
#include //引用头文件
#pragma comment(lib,"ws2_32.lib") //链接库文件
using namespace std;
int main(void)
{
    WSADATA wsd;                       //定义WSADATA对象
/*
    WSAStartup错误码介绍

    WSASYSNOTREADY  网络通信中下层的网络子系统没准备好

    WSAVERNOTSUPPORTED  Socket实现提供版本和socket需要的版本不符

    WSAEINPROGRESS  一个阻塞的Socket操作正在进行

    WSAEPROCLIM  Socket的实现超过Socket支持的任务数限制

    WSAEFAULT   lpWSAData参数不是一个合法的指针
*/

/*
    当一个应用程序调用WSAStartup函数时,操作系统根据请求的Socket版本来搜索相应的Socket库,然后绑定找到的Socket库到该应用程序中。以后应用程序就可以调用所请求的Socket库中的其它Socket函数了。该函数执行成功后返回0。
例:假如一个程序要使用2.2版本的Socket,那么程序代码如下
*/
//WSAStartup(MAKEWORD(2, 2), &wsd);    //初始化套接字
    WSAStartup(MAKEWORD(2, 2), &wsd);
    SOCKET m_SockClient;               //创建socket对象
/*

    sockaddr_in的定义如下
    struct   sockaddr_in
    {
        short   int   sin_family;     // Address family 一般来说 AF_INET(地址族)PF_INET(协议族 )
        unsigned   short   int   sin_port;  //sin_port存储端口号(使用网络字节顺序),在linux下,端口号的范围0~65535,同时0~1024范围的端口号已经被系统使用或保留
        struct   in_addr   sin_addr;     //存储IP地址
        unsigned   char   sin_zero[8];  //sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节
    };
*/
    sockaddr_in clientaddr;           //服务器信息 

    clientaddr.sin_family = AF_INET;        
    clientaddr.sin_port = htons(4600);
    clientaddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    /*
        socket的定义如下
        SOCKET WSAAPI socket(
        _In_ int af,
        _In_ int type,
        _In_ int protocol
        );

        建立一个socket用于连接
        af:address family,如AF_INET
        type:连接类型,通常是SOCK_STREAM或SOCK_DGRAM
        protocol:协议类型,通常是IPPROTO_TCP或IPPROTO_UDP
        返回值:socket的编号,为-1表示失败
    */
    m_SockClient = socket(AF_INET, SOCK_STREAM, 0);

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

     第一个参数是客户端的套接字(表明即将发起连接请求),第二个参数是服务端的套接字所在的“地方”(“地方”是我自定义的专有名词),
第三个参数是该“地方”的大小
     如果请求连接成功,则返回0,否则返回错误码。
*/

    int i = connect(m_SockClient, (sockaddr*)&clientaddr, sizeof(clientaddr));     
    cout << "Connection status  " << i << endl;

    char buffer[1024];
    char inBuf[1024];
    int num;
    num = recv(m_SockClient, buffer, 1024, 0);             //阻塞函数,等待接受内容
    if (num > 0)                //阻塞
    {
        cout << "Receive form server" << buffer << endl;
        while (true)
        {
            num = 0;
            cin >> inBuf;
            if (!strcmp(inBuf, "exit"))                     //如果输入的是exit则断开连接
            {
                send(m_SockClient, inBuf, sizeof(inBuf), 0);
                return 0;
            }
            send(m_SockClient, inBuf, sizeof(inBuf), 0);
            num = recv(m_SockClient, buffer, 1024, 0);
            if (num >= 0)
                cout << "Receive form server: " << buffer << endl;  //输出接受到的内容
        }
    }
}` 

*   1
*   2
*   3
*   4
*   5
*   6
*   7
*   8
*   9
*   10
*   11
*   12
*   13
*   14
*   15
*   16
*   17
*   18
*   19
*   20
*   21
*   22
*   23
*   24
*   25
*   26
*   27
*   28
*   29
*   30
*   31
*   32
*   33
*   34
*   35
*   36
*   37
*   38
*   39
*   40
*   41
*   42
*   43
*   44
*   45
*   46
*   47
*   48
*   49
*   50
*   51
*   52
*   53
*   54
*   55
*   56
*   57
*   58
*   59
*   60
*   61
*   62
*   63
*   64
*   65
*   66
*   67
*   68
*   69
*   70
*   71
*   72
*   73
*   74
*   75
*   76
*   77
*   78
*   79
*   80
*   81
*   82
*   83
*   84
*   85
*   86
*   87
*   88
*   89
*   90
*   91
*   92
*   93
*   94
*   95
*   96
*   97
*   98
*   99
*   100
*   101
*   102
*   103
*   104

在windows中的运行结果如图:
C++ socket通讯详解及注意事项_第5张图片

你可能感兴趣的