c++网络编程2:TCP连接概念及编程

一.TCP建立连接的三次握手

在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。

第一次握手:建立连接时,客户端发送SYN包(SYN=j)到服务器,并进入SYN_SEND状态,等待服务器确认;【客户端->服务端:SYN(j)】

第二次握手:服务器收到SYN包,必须确认客户的SYN(ACK=j+1),同时自己也发送一个SYN包(SYN=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;【服务端->客户端:SYN(k),ACK(j+1)】

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ACK=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。【客户端->服务端:ACK(k+1)】

完成三次握手,客户端与服务器开始传送数据。上述是一个完整的TCP连接过程。

然则为什么必然要进行三次握手来包管连接是双工的呢,一次不可么?两次不可么?我们举一个实际生活中的两个人进行说话沟通的例子来模仿三次握手。

第一次对话:

老婆让甲出去打酱油,半路碰着乙,甲问了一句:哥们你吃饭了么?

结果乙带着耳机听歌呢,根本没听到,没反响。甲心里想:跟你说话也没个音,不跟你说了,沟通失败。说明乙接管不到甲传过来的信息的情况下沟通必然是失败的。

若是乙听到了甲说的话,那么第一次对话成功,接下来进行第二次对话。

第二次对话:

乙听到了甲说的话,然则他是老外,中文不好,不知道甲说的啥意思也不知道如何答复,于是随便答复了一句学过的中文 :我去厕所了。甲一听立即笑喷了,“去厕所吃饭”?道不合不相为谋,离你远点吧,沟通失败。说明乙无法做出正确应答的情况下沟通失败。

若是乙听到了甲的话,做出了正确的应答,并且还进行了反问:我吃饭了,你呢?那么第二次握手成功。

经由过程前两次对话证了然乙可以或许听懂甲说的话,并且能做出正确的应答。接下来进行第三次对话。

第三次对话:

甲刚和乙打了个招呼,忽然老婆喊他,“你个死鬼,打个酱油咋这么半天,看我回家咋收拾你”,甲是个妻管严,听完吓得二话不说就跑回家了,把乙本身晾那了。乙心想:这什么人啊,得,我也回家吧,沟通失败。说明甲无法做出应答的情况下沟通失败。

若是甲也做出了正确的应答:我也吃了。那么第三次对话成功,两人已经建立起了顺畅的沟通渠道,接下来开始聊天。

经由过程第二次和第三次的对话证明甲可以或许听懂乙说的话,并且能做出正确的应答。

可见,两个人进行有效的说话沟通,这三次对话的过程是必须的。

同理对于TCP为什么须要进行三次握手我们可以一样的懂得:

为了让服务端能接收到客户端的信息并能做出正确的应答而进行前两次(第一次和第二次)握手,为了让客户端可以接收到服务端的信息并能做出正确的应答而进行后两次(第二次和第三次)握手。

TCP半开连接数概念:

在完成三次握手之后,TCP连接已经建立,那么对系统来说,这就是一个TCP连接数。系统对这样一个 没有限-制。如果三次握手没有完成,客户端与服务器没有开始传送数据,那么,这样试图完成三次握手的事件发生 的 是被系统所限-制的。而这个数就是所谓的半开连接数了。

二.TCP客户端的编程

1.首先加载系统的套接字库

[cpp]view plaincopyprint?

  1. WORD wVersionRequested;
  2. wVersionRequested=MAKEWORD(1,1);//指定Winsock库的版本:1.1
  3. WSADATA wsaData;
  4. int err;
  5. //使用WSAStartup加载套接字库,以及确定使用的套接字库的版本,这里使用的是1.1的版本
  6. err=WSAStartup(wVersionRequested,&wsaData);
  7. if(err!=0)
  8. return false;
  9. if(LOBYTE(wsaData.wVersion)!=1||HIBYTE(wsaData.wVersion)!=1)
  10. {
  11. //如果套接字库加载成功,但是版本不正确,那么调用WSACleanup 释放对套接字库的占用资源
  12. WSACleanup();
  13. return false;
  14. }

2.创建客户端的套接字

[cpp]view plaincopyprint?

  1. SOCKET sockClient;
  2. unsigned long mode=1;//0:阻塞
  3. sockClient=socket(AF_INET,SOCK_STREAM,0);

注意:套接字是通讯的基础,可以认为套接字是程序员对一个通讯端进行操作的对象,比如说你想将本机的客户端连接到远程机器,就需要使用客户端产生的套接字去连接,你操作的对象就是套机字,他是一个很抽象的概念,但是确实编程所必须的,每个要进行通讯的终端(不管是服务器还是客户端)都必须产生一个套接字。

3.将套接字绑定到本地的某个地址和端口上;在当前客户端的系统上开启一个端口与服务器上的端口进行数据交互,这个过程称为端口的绑定,TCP客户端可以不需要这个过程,因为在调用connect接口连接服务的时候会自动开启一个端口,当然如果有特殊需求必须要绑定一个端口的话,就这样做:

[cpp]view plaincopyprint?

  1. //sockClient为客户端套接字接口,localIP为客户端的IP(可以不填),localPort为客户端需要打开的端口
  2. bool BindSocketEx(SOCKET sockClient,LPCSTR localIP,UINT localPort)
  3. {
  4. //LPCTSTR在unicode环境下是const wchar *,非unicode环境下是const char *
  5. //INADDR_ANY表示如果一台机器上(服务器)有多块网卡,每个网卡都有各自的IP,则INADDR_ANY
  6. //会将套接字绑定到该机器上的所有网卡地址和端口上
  7. SOCKADDR_IN addrSrv;
  8. if(localIP==NULL)
  9. addrSrv.sin_addr.S_un.S_addr= htonl(INADDR_ANY);//由于这里需要网络字节序,所以将主机字节序转换成网络字节序
  10. else
  11. addrSrv.sin_addr.S_un.S_addr=inet_addr(localIP);//inet_addr将字符串ip转换成long型
  12. addrSrv.sin_family=AF_INET;
  13. addrSrv.sin_port=htons(localPort);//由于这里需要网络字节序,所以将主机字节序转换成网络字节序
  14. //下面的代码等效于这行代码return bind(sockClient,(SOCKADDR *)&addrSrv,sizeof(SOCKADDR))==0;
  15. if(bind(sockClient,(SOCKADDR *)&addrSrv,sizeof(SOCKADDR))==0)
  16. return true;
  17. else
  18. return false;
  19. }

注意:网络字节序和主机字节序,这里牵扯到机器本身的大端模式(其实地址存放高位字节)和小端模式(起始地址存放低位字节),intel的CPU是属于小端模式,而网络字节序是大端模式,所以需要使用htons将主机字节序转换成网络字节序。

网络->主机:ntohs ntohl 主机->网络:htons htonl(h:主机 n:网络 s:short l:long)

4.连接服务器

[cpp]view plaincopyprint?

  1. //sockClient为客户端的套接字,svrIP为服务器IP,svrPort为服务器的端口
  2. bool TCP_Client_ConnectSocketEx(SOCKET sockClient,LPCSTR svrIP,USHORT svrPort)
  3. {
  4. if(svrIP==NULL||svrPort==0)
  5. return NULL;
  6. SOCKADDR_IN addrSrv;
  7. addrSrv.sin_addr.S_un.S_addr=inet_addr(svrIP);//服务器IP,inet_addr将字符串ip转换成long型
  8. addrSrv.sin_family=AF_INET;
  9. addrSrv.sin_port=htons(svrPort);//服务器端口,主机字节序转换成网络字节序
  10. //调用connect 内部实现对TCP的三次握手建立连接 返回0表示成功
  11. return connect(sockClient,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR))==0;
  12. }

这里的connect是阻塞的方式,如果网络问题,无法连接上服务器,那么connect就会等待过长的时间,所以如果不想造成这种后果,就应该把socket置为非阻塞的方式,具体操作如下:

[cpp]view plaincopyprint?

  1. //sockClient为客户端的套接字,svrIP为服务器IP,svrPort为服务器的端口
  2. bool TCP_Client_ConnectSocketEx(SOCKET sockClient,LPCSTR svrIP,USHORT svrPort)
  3. {
  4. if(svrIP==NULL||svrPort==0)
  5. return NULL;
  6. unsigned long on = 1;
  7. struct timeval timeout ;
  8. fd_set r;
  9. int ret;
  10. ioctlsocket(hSocket, FIONBIO, &on);//将套接字设为非阻塞,on=1为非阻塞,on=0阻塞
  11. SOCKADDR_IN addrSrv;
  12. addrSrv.sin_addr.S_un.S_addr=inet_addr(svrIP);//服务器IP,inet_addr将字符串ip转换成long型
  13. addrSrv.sin_family=AF_INET;
  14. addrSrv.sin_port=htons(svrPort);//服务器端口,主机字节序转换成网络字节序
  15. //调用connect 内部实现对TCP的三次握手建立连接 返回0表示成功
  16. connect(sockClient,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
  17. FD_ZERO(&r);
  18. FD_SET(sockClient, &r);
  19. timeout.tv_sec = 5000/1000; //连接超时5秒
  20. <timeout.tv_usec =TimeOut%1000;
  21. ret = select(0, 0, &r, 0, &timeout);//select阻塞5秒 连接超时返回小于等于0的值
  22. if ( ret <= 0 )
  23. {
  24. closesocket( sockClient);
  25. return false;
  26. }
  27. return true;
  28. }

5.客户端接受数据(从服务器发过来的)

[cpp]view plaincopyprint?

  1. int RLen=0;
  2. RLen=recv(sockClient,(char *)RecvBuffer,RecvBufferLen,0);
  3. if(RLen<=0)
  4. return false;

RecvBuffer是接受到的数据,RecvBufferLen是需要一次接收的数据长度,RLen是实际接收的数据长度,客户端如果想要不停的接受从服务器返回的数据,就必须开个线程,将recv放入线程中接收数据

注意: 虽然这里可以设置一次接收的数据长度RecvBufferLen,但是这个长度必须需要系统缓存的支持,系统有一个接收数据的缓存,这个缓存负责将网络传过来的数据放入其中,然后recv是从这个系统缓存取数据,系统默认的SOCKET接受缓存的大小为8688B(8.6K左右),如果sock函数recv或recvFrom从系统缓存中接受数据太慢,那么系统接受到新数据后就会将原缓存给覆盖了,这样在次调用recv函数就获取到了新数据,老数据就丢失了,这时候就要使用setsockopt方法将系统缓存开到32K最合适,如下:

[cpp]view plaincopyprint?

  1. if(setsockopt(sockClient,SOL_SOCKET,SO_RCVBUF,(const char *)&Max_RecvFrom_Buffer_Size,sizeof(int))!=0)
  2. {
  3. //设置失败
  4. return INVALID_SOCKET;
  5. }

Max_RecvFrom_Buffer_Size=32*1024B,在创建一个客户端的套接字之后就要使用setsockopt来设置系统缓存的大小了,也就是第2部做完后就要设置系统缓存

6.客户端发送数据(给服务器)

[cpp]view plaincopyprint?

  1. int Ret;
  2. Ret =send(sockClient,SendBuffer,sendBufLen,0);
  3. if ( Ret == SOCKET_ERROR )
  4. {
  5. int err = GetLastError( );
  6. if ( err == WSAEWOULDBLOCK || err == WSAETIMEDOUT )
  7. {
  8. Sleep(1);
  9. }
  10. else //网络断了
  11. {
  12. Ret = 0;
  13. return false;
  14. }
  15. }

SendBuffer是发送的数据,sendBufLen是一次发送数据的长度,这里的长度要考虑网络最大的传输单元MTU=1500B,发送的数据长度控制在1400B较好。

三.TCP服务端的编程

1.加载套接字(同客户端)

2.创建服务端套接字socketService(同客户端)

3.在服务端绑定套接字(同客户端,但是服务端是一定要绑定套接字的,也就是开启监听端口)

4.将服务端的套接字设为监听模式

[cpp]view plaincopyprint?

  1. bool ListenSocketEx(SOCKET socketService,UINT connectNums)
  2. {
  3. if(connectNums==0)
  4. return listen(socketService,SOMAXCONN)==0;
  5. else
  6. return listen(socketService,connectNums)==0;
  7. }

connectNums为客户端连接最大数,SOMAXCONN为最大连接数

5.上面四步服务端已经将基本的初始化了,接下来是最重要的一步,开启线程,然后调用接口accept监听客户端是否有连接过来,服务端其实就是一个等待的过程,等待客户端的连接,然后处理数据,在发给客户端

[cpp]view plaincopyprint?

  1. UINT ClientSocketListenProc(LPVOID lpParameter)
  2. {
  3. while(true)
  4. {
  5. SOCKADDR_IN addrClient;
  6. int addrDataLen=sizeof(SOCKADDR);//非常重要
  7. SOCKET clientSock=accept(socketService,(sockaddr *)&addrClient,&addrDataLen);
  8. }
  9. }

accept返回的是连接客户端的socket,这个socket有什么用呢??服务端创建了一个socket,但是如果很多客户端连接服务端,服务端在发送给客户端数据和接收来自客户端数据的时候使用服务端创建的这一个socket显然不合理,因为它不知道发给哪个客户端,这时候就需要accept返回的客户端socket,这个返回的clientSocket是唯一的,服务端使用它来发送和接收数据,就能保证数据的正确性。所以accept返回的socket服务端需要保存起来,使用map容器保存,这里不写代码。

注意:accept是阻塞模式的,它的第三个参数非常的重要,如果填的不是SOCKADDR的长度&addrDataLen,就会收到一些非法连接的客户端,然后accept就会不停的接受204.204.204.204这个IP来的连接

6.接收客户端发的数据

[cpp]view plaincopyprint?

  1. int RLen=0;
  2. RLen=recv(acptClientSocket,(char *)RecvBuffer,RecvBufferLen,0);
  3. if(RLen<=0)
  4. return false;

这里的接收数据和客户端一样,注意的是recv第一个参数socket是accept返回的客户端连接socket----acptClientSocket,不是服务端创建的socket

7.发送数据给客户端

[cpp]view plaincopyprint?

  1. int Ret;
  2. Ret =send(acptClientSocket,SendBuffer,sendBufLen,0);
  3. if ( Ret == SOCKET_ERROR )
  4. {
  5. int err = GetLastError( );
  6. if ( err == WSAEWOULDBLOCK || err == WSAETIMEDOUT )
  7. {
  8. Sleep(1);
  9. }
  10. else //网络断了
  11. {
  12. Ret = 0;
  13. return false;
  14. }
  15. }

这里的发送数据和客户端一样,注意的是send第一个参数socket是accept返回的客户端连接socket----acptClientSocket,不是服务端创建的socket