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协议族中的一种。
    这里有一张图,表明了这些协议的关系。

C++ socket通讯详解及注意事项_第1张图片
TCP/IP协议族包括运输层、网络层、链路层。现在你知道TCP/IP与UDP的关系了吧。
Socket在哪里呢?
在图1中,我们没有看到Socket的影子,那么它到底在哪里呢?还是用图来说话,一目了然。
C++ socket通讯详解及注意事项_第2张图片

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

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

客户端向服务器发送一个SYN J
服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
客户端再想服务器发一个确认ACK K+1
只有就完了三次握手,但是这个三次握手发生在socket的那几个函数中呢?请看下图:
C++ socket通讯详解及注意事项_第3张图片
从图中可以看出,当客户端调用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通讯详解及注意事项_第4张图片
图示过程如下:
某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
接收到这个FIN的源发送端TCP对它进行确认。
这样每个方向上都有一个FIN和ACK。

socket通信流程

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

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

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:"<<Ip[iConnect]<< " disconnected!" << endl<<endl;
			break;
		}
		if (num >= 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:"<<Ip[iConnect]<<"is: " << sendBuffer << endl << endl<<endl;
		}
		else if (!strcmp(buffer, "CCC"))      //如果接受到 CCC 返回 DDD
		{
     
			memset(sendBuffer, 0, 1024);
			strcpy(sendBuffer, "DDD");
			int ires = send(hsock, sendBuffer, sizeof(sendBuffer), 0);         //回送信息
			cout << "The message sent to IP:" << Ip[iConnect] << "is: " << sendBuffer << endl << endl << endl;
		}
		else if (!strcmp(buffer, "exit"))      //如果接受到 exit 结束该进程
		{
     
			cout << "Client with IP:" << Ip[iConnect] << " disconnected!" << endl;
			cout << "Server Process Close:  " << endl<<endl;
			return 0;
		}
		else                //如果接受到 其它指令 返回 Commend error
		{
     
			memset(sendBuffer, 0, 1024);
			strcpy(sendBuffer, "Commend error");
			int ires = send(hsock, sendBuffer, sizeof(sendBuffer), 0);
			cout << "The message sent to IP:" << Ip[iConnect] << "is: " << sendBuffer << endl << endl << endl;
		}
	}
	return 0;
};
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);    //初始化套接字
	
	if (WSAStartup(MAKEWORD(2, 2), &wsd))  
	{
     
		printf("Initlalization Error!");
		return -1;
	}

	SOCKET m_SockServer;            //创建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 serveraddr;         //创建sockaddr_in对象储存自身信息
	sockaddr_in serveraddrfrom;

	SOCKET m_Server[20];            //创建socket数组来存放来自客户端的信息最大连接数为20

	serveraddr.sin_family = AF_INET;             //设置服务器地址家族
	serveraddr.sin_port = htons(4600);           //设置服务器端口号

/*
	inet_addr语法如下
	unsigned long PASCAL FAR inet_addr( const struct FAR* cp);
	
	本函数解释cp参数中的字符串,这个字符串用Internet的“.”间隔格式表示一个数字的Internet地址。
返回值可用作Internet地址。所有Internet地址以网络字节顺序返回(字节从左到右排列)
*/
	serveraddr.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_SockServer = socket(AF_INET, SOCK_STREAM, 0);    //创建一个临时变量并赋值给m_SockServer

/*
	bind的语法:int bind( int sockfd, struct sockaddr* addr, socklen_t addrlen)
	返回:0──成功, -1──失败 
	
	参数sockfd
	指定地址与哪个套接字绑定,这是一个由之前的socket函数调用返回的套接字。调用bind的函数之后,该套接字与一个相应的地址关联,
发送到这个地址的数据可以通过这个套接字来读取与使用
	
	参数addr
	指定地址。这是一个地址结构,并且是一个已经经过填写的有效的地址结构。调用bind之后这个地址与参数sockfd指定的套接字关联,
从而实现上面所说的效果
	
	参数addrlen
	正如大多数socket接口一样,内核不关心地址结构,当它复制或传递地址给驱动的时候,它依据这个值来确定需要复制多少数据。
这已经成为socket接口中最常见的参数之一了

*/
	int i = bind(m_SockServer, (sockaddr*)&serveraddr, sizeof(serveraddr));    //把名字和套接字绑定
	cout << "bind:" << i << endl<<endl;

	int iMaxConnect = 20;           //最大连接数
	int iLisRet;
	char buf[] = "THIS IS SERVER\0";
	char WarnBuf[] = "It,is voer Max connect\0";
	int len = sizeof(serveraddr);            //serveraddr所占的字节大小

	while (true)
	{
     
/*
	listen函数在一般在调用bind之后-调用accept之前调用,语法如下:
	int listen(int sockfd, int backlog)
	返回:0──成功, -1──失败

	参数sockfd
	被listen函数作用的套接字,sockfd之前由socket函数返回。在被socket函数返回的套接字fd之时,它是一个主动连接的套接字,也就是此时系统假设用户会对这个套接字调用connect函数,
期待它主动与其它进程连接,然后在服务器编程中,用户希望这个套接字可以接受外来的连接请求,也就是被动等待用户来连接。由于系统默认时认为一个套接字是主动连接的,
所以需要通过某种方式来告诉系统,用户进程通过系统调用listen来完成这件事
	
	参数backlog
	这个参数涉及到一些网络的细节。在进程正理一个一个连接请求的时候,可能还存在其它的连接请求。因为TCP连接是一个过程,所以可能存在一种半连接的状态,
有时由于同时尝试连接的用户过多,使得服务器进程无法快速地完成连接请求。如果这个情况出现了,服务器进程希望内核如何处理呢?
内核会在自己的进程空间里维护一个队列以跟踪这些完成的连接但服务器进程还没有接手处理或正在进行的连接,这样的一个队列内核不可能让其任意大,所以必须有一个大小的上限。这个backlog告诉内核使用这个数值作为上限。
服务器进程不能随便指定一个数值,内核有一个许可的范围。这个范围是实现相关的。很难有某种统一,一般这个值会小30以内
*/
		iLisRet = listen(m_SockServer, 0);    //进行监听

/*

	accept语法如下:
	SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen);
	
	它的参数与 listen() 和 connect() 是相同的:sock 为服务器端套接字,addr 为 sockaddr_in 结构体变量,
addrlen 为参数 addr 的长度,可由 sizeof() 求得
	
	accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,
大家注意区分。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。

	accept()系统调用主要用在基于连接的套接字类型,比如SOCK_STREAM和SOCK_SEQPACKET。它提取出所监听套接字的等待连接队列中第一个连接请求,
创建一个新的套接字,并返回指向该套接字的文件描述符。新建立的套接字不在监听状态,原来所监听的套接字也不受该系统调用的影响

备注:新建立的套接字准备发送send()和接收数据recv()

*/
		int temp=0;
		int Len = sizeof(serveraddrfrom);
		m_Server[iConnect] = accept(m_SockServer, (sockaddr*)&serveraddrfrom, &len);

		if (m_Server[iConnect] != INVALID_SOCKET)  //INVALID_SOCKET表示无效 
		{
     
			//int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen)

			if (getsockname(m_Server[iConnect], (struct sockaddr*)&serveraddrfrom, &Len) != -1)
			{
     
				
				printf("listen address = %s:%d\n", inet_ntoa(serveraddrfrom.sin_addr), ntohs(serveraddrfrom.sin_port));
				sprintf(Ip[iConnect], "%s", inet_ntoa(serveraddrfrom.sin_addr));
			}
			else
			{
     
				printf("getsockname error\n");
				exit(0);
			}
			

/*
	send语法如下:
	int send( SOCKET s, const char FAR *buf, int len, int flags );  
    
	s:指定发送端套接字描述符;
    
	*buf:指明一个存放应用程序要发送数据的缓冲区;

    len:指明实际要发送的数据的字节数;
	
	flags:一般置0。 

	不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。客户程序一般用send函数向服务器发送请求,
而服务器则通常用send函数来向客户程序发送应答。

*/
			int ires = send(m_Server[iConnect], buf, sizeof(buf), 0);                    //发送字符过去
			cout << "accept" << ires << endl<<endl;                                            //显示已经连接次数                         
			iConnect++;
			if (iConnect > 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动态链接库初始化时分配的资源
}

客户端代码

#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;  //输出接受到的内容
		}
	}
}

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

注意事项

TCP 建立连接以后,双方就是对等的。不论是哪一方,只要正常 close(socket_handle),那么 TCP 底层软件都会向对端发送一个 FIN 包。
FIN 包到达对方机器之后,对方机器的 TCP 软件会向应用层程序传递一个 EOF 字符,同时自动进入断开连接流程(要来回协商几次,但这些都是自动的、不可控的)。什么是 EOF 字符?它其实什么也不是,只是一个标记,上层应用程序如果这时读 socket 句柄的话,就会读到 EOF,也就是说,此时 socket 句柄看起来里面有数据,但是读不出来,因此 select 返回可读(非阻塞模式下)read 不会阻塞(阻塞模式下)但是 read 的返回值却是 0。
accept(),read()和receive() 都是堵塞的
我们可以使用Socket类、ServerSocket类和DatagramSocket类的setSoTimeout()方法,
设置其阻塞的最长时间(以毫秒为单位)。
如果在指定时间内这些方法没有返回,则将抛出一个InterruptedIOException异常

对于Socket实例,在调用read()方法前,我们还可以使用该Socket的InputStream.available()方法来检测是否有可读的数据。
超时设置为0表示该操作永不超时。如果阻塞超过了超时时长,则抛出一个异常。InterruptIOException

连接超时 Socket socket = new Socket(server, servPort);
会尝试根据参数中指定的主机和端口来建立连接,并阻塞等待,直到连接成功建立或发生了系统定义的超时。
系统定义的超时时间很长,而Java又没有提供任何缩短它的方法。
解决:
Socket socket = new Socket();//创建一个无连接的Socket
socket.connect(soctetAdress,timeout);

如果一段时间内没有数据交换,通信的每个终端可能都会怀疑对方是否还处于活跃状态。
TCP协议提供了一种keep-alive的机制,该机制在经过一段不活动时间后,将向另一个终端发送一个探测消息,如果另一个终端还出于活跃状态,它将回复一个确认消息。
如果经过几次尝试后依然没有收到另一终端的确认消息,则终止发送探测信息,关闭套接字,并在下一次尝试I/O操作时抛出一个异常。

注意,应用程序只要在探测信息失败时才能察觉到keep-alive机制的工作。

默认情况下,keep-alive机制是关闭的。通过调用socket.setKeepAlive(true)来开启keep-alive机制。
一旦创建了一个Socket或DatagramSocket实例,操作系统就必须为其分配缓存区以存放接收的和要发送的数据。
Socket, DatagramSocket: 设置和获取发送接收缓存区大小
int getReceiveBufferSize()
void setReceiveBufferSize(int size)
int getSendBufferSize()
void setSendBufferSize(int size)
这里指定的大小只是作为一种建议给出的,实际大小可能与之存在差异。

ServerSocket: 设置/获取所接受套接字的接收缓冲区大小
int getReceiveBufferSize()
void setReceiveBufferSize(int size)
实际上用于获取和设置由accept()方法创建的Socket实例的接收缓冲区的大小(字节)。
TCP协议将数据缓存起来直到足够多时一次发送,以避免发送过小的数据包而浪费网络资源。

虽然这个功能有利于网络,但有些应用程序可能对所造成的缓冲延迟不能容忍
socket.getTcpNoDelay()和socket.setTcpNoDelay()方法用于获取和设置是否消除缓冲延迟,将值设置为true表示禁用缓冲延迟功能。

socket.setSoLinger(true),那么后面再调用的close()方法将阻塞等待,
直到远程终端对所有数据都返回了确认信息,或者发生了指定的超时(秒)。
如果发生了超时,TCP连接将强行关闭。
如果开启了停留功能,getSoLinger()方法将返回指定的超时时间,否则返回-1。

本篇到此结束,有什么需要补充的欢迎在评论区留言哦~
群内有各种学习资料,欢迎大家一起来学习~
如果大家遇到什么问题也欢迎大家进群讨论~
qq群:759252814
期待你的关注~
感谢大家的支持,谢谢!

你可能感兴趣的:(C++,c++)