TCP程序设计基础

2015/05/08 计算机网络

TCP协议是一种面向连接的协议,在传输数据之前必须先建立连接,通信完成后还需要释放连接,连接的建立和释放是由通信双方相互协作共同完成的。主动发起连接建立的一端称为“客户端”,被动接收连接请求的一端被称为“服务器”。

TCP客户端和服务器端的交互过程

通常,服务器大都可以同时与多个客户保持连接,因此,在通信过程中,服务器端至少有两个套接字,其中有一个被称为监听套接字,该套接字由服务器程序调用socket()函数创建,专用于等待客户连接请求的到来;其余套接字则是连接建立时由系统创建的,称为已连接套接字,每当监听套接字接收到一个客户连接请求,系统就会为该连接请求创建一个新的已连接套接字,并用该套接字与客户端建立连接。以后,与客户之间的数据交换也要使用这个已连接套接字。已连接套接字绑定的IP地址来自于监听套接字,因此,与监听套接字相同。端口号则是由系统自动分配的,是系统从未使用的端口号中随机选择的。

一般,服务器进程首先启动,并等待客户端的连接建立请求,其基本通信过程如下:

(1).调用socket函数建立一个套接字,该套接字用于监听。

(2).调用bind函数为套接字绑定一个端口号和IP地址。

(3).调用listen函数,设置套接字处于监听状态。

(4).如果程序不退出,则反复执行:

a.使用accept函数等待客户机的连接到来。如果有远程计算机的连接请求到来,则用一个新的套接字(已连接套接字)建立起与客户机之间的通信连接。

b.使用recv函数和send函数利用新建的连接和客户端通信。

c.通信完毕调用closesocket函数关闭连接。

客户端程序的流程如下:

(1).用socket函数建立一个套接字,设定服务器的IP和端口。

(2).调用connect函数连接远程计算机指定的端口。

(3).使用recv函数和send函数利用新建的连接与服务器通信。

(4).通信完毕调用closesocket函数关闭连接。

流式套接字程序的服务器和客户工作流程如图:

创建套接字

创建套接字的实质就是请求操作系统分配通信所需要的一些系统资源,包括存储空间、网络资源、CPU时间等,这些资源的总和用一个称为套接字描述符的整数表示。创建套接字需要调用socket函数完成。

函数原型:SOCKET socket(int af,int type,int protocol);

函数参数:

af:标识一个地址家族,该函数的取值通常为表示TCP/IP协议地址族的常量AF_INET。

type:标识套接字类型,SOCK_STREAM表示流式套接字,SOCK_DGRAM表示数据报套接字,SOCK_RAW表示原始套接字。

protocol:用于指定套接字所用的特定协议,依赖于第2个参数type,设为0,表示使用默认的协议。

socket函数返回的套接字描述符的类型符SOCKET,它是Winsock中专门定义的一种新的数据类型,其定义为:typedef u_int SOCKET;

使用socket函数创建流式套接字可以使用如下代码:

SOCKET sock_server;
if((sock_server=socket(AF_INET,SOCK_STREAM,0))==INVALID_SOCKET){
	cout<<"创建套接字失败!错误代码:"<<WSAGetLastError()<<endl;
	WSACleanup();
	return 0;
}

注意,服务程序和客户程序所创建的套接字的用处是不同,服务器创建的套接字是监听套接字,并不用于数据收发,而客户端创建的套接字则有两方面用途:一是向服务器发送连接建立请求,二是用于进行客户端的数据收发。

绑定地址

socket函数在创建套接字时并没有为创建的套接字分配地址,因此服务器软件在创建了监听套接字以后,还必须为它分配一个地址,该地址为套接字指定需要监听的TCP端口号和IP地址。将套接字绑定到一个指定的地址上的套接字函数是bind()。

函数原型:int bind(SOCKET s,const struct sockaddr * name,int namelen)

函数参数:

s:是一个套接字。

name:是一个sockaddr结构体指针,该结构体包含了要绑定的地址和端口号,该结构体指针类型在Winsock中已被定义为一种新的类型LPSOCKADDR:

typdef sockaddr * LPSOCKADDR;

namelen:确定name缓冲区的长度。

该函数要绑定的地址是一个sockaddr类型的指针参数指定的,编程时一般先定义一个sockaddr_in结构的变量,在填充完地址信息后再将该结构变量的地址强制转换为sockaddr类型的指针来使用。

.
.
.
#define PORT 65432   //指定本程序监听的TCP端口号
.
.
.
struct sockaddr_in addr;          //定义sockaddr_in结构变量addr
memset((void*)&addr,0,addr_len);  //将addr的各个字段值全部置0
addr.sin_family=AF_INET;          //协议族为AF_INET
addr.sin_port=htons(PORT);        //端口号为常量PORT
addr.sin_addr.s_addr=htonl(INADDR_ANY);  //监听本机分配的所有IP地址
if(bind(sock_server,(struct sockaddr *)&addr,sizeof(addr))!=0)  //绑定地址
{
	cout<<"绑定失败!错误代码:"<<WSAGetLastError()<<endl;
	closesocket(sock_server);     //关闭套接字
	WSACleanup();                 //注销WinSock库
	return 0;
}

上述代码中将IP地址设置为INADDR_ANY,是表示该套接字的IP地址由系统自动指定,如果你希望自己明确指定一个IP地址,则只要将INADDR_ANY替换为一个无符号整数表示的IP地址即可。如果你希望由系统自动给你分配一个端口号,则只要将PORT定义为0则可,否则,PORT应定义为一个用16位无符号整数表示的可用端口号。

注意:

客户端的套接字可以不用绑定地址,当客户端程序调用connect()函数与服务器建立连接时,系统会为套接字自动旋转一个本机IP地址和未使用的端口号。事实上,服务器端的监听套接字不绑定地址也不会出现明显错误,因为当服务器调用listen()时,系统也会为监听套接字分配IP地址和临时TCP端口号,不过由于临时端口号很难被客户知晓从而导致客户无法访问服务器,因此服务器端需要绑定指定的地址。

开始监听

服务器端在绑定地址之后,便可调用listen()函数进入监听状态,等待客户端的连接请求。listen()函数是只能由服务器端使用的函数,而且只适用于流式套接字。

函数原型:int listen(SOCKET s,int backlog)

函数参数:

s:用来监听客户连接请求的套接字的描述符。

backlog:表示等待连接的最大队列长度。例如,如果backlog被放置为3,此时又4个客户同时发出连接请求,那么前3个客户端连接会放置在等待队列中,第4个客户端会得到连接错误信息。该参数值通常设置为常量SOMAXXCONN,表示将连接队列的最大长度值设置为一个最大的“合理”值。

if(listen(sock_server,0)!=0){   //监听端口
	cout<<"listen调用失败!错误代码:"<<WSAGetLastError<<endl;
	closesocket(sock_server);   //关闭套接字
	WSACleanup();               //注销WinSock库
	return 0;
}esle{
	cout<<"listen....."<<endl;  //调用成功

TCP的连接过程

在介绍TCP的连接过程之前,必须先知道TCP的报文格式。首先是TCP报文格式图:

上图中有几个字段需要重点介绍下:

(1)序号:Seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。

(2)确认序号:ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,ack=Seq+1。

(3)标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下:

(A)URG:紧急指针(urgent pointer)有效。

(B)ACK:确认序号有效。

(C)PSH:接收方应该尽快将这个报文交给应用层。

(D)RST:重置连接。

(E)SYN:发起一个新连接。

(F)FIN:释放一个连接。

需要注意的点:

1.不要将确认序号ack与标志位中的ACK搞混;

2.当确认方的ack等于发起方seq+1,两端才能配对。

三次握手:

所谓三次握手(Three-Way Handshake)即建立TCP连接,就是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发,整个流程如下图所示:

(1)第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client由CLOSED状态进入SYN_SENT状态,等待Server确认。这部分对应于客户端程序调用connect()函数向服务器发送链接请求报文

(2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。这部分对应于服务器调用accept()函数,从listen()的连接请求等待队列中取出一个连接请求,并为之创建套接字并分配资源,开始建立连接

(3)第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Client进入ESTABLISHED状态,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。这部分对应于客户端对服务器发来的确认发送一个确认,connect()函数返回,连接建立成功

更为详细的三次握手流程图:

总结:

主机A通过向主机B发送一个含有同步序列号的标志位的数据段给主机B,向主机B请求建立连接,通过这个数据段,主机A告诉主机B:我想要和你通信;

主机B收到主机A的请求后,用一个带有确认应答(ACK)和同步序列号(SYN)标志位的数据段响应主机A,也告诉主机A两件事:我已经收到你的请求了,你可以传输数据了;

主机A收到这个数据段后,再发送一个确认应答,确认已收到主机B的数据段:我已收到回复,我现在要开始传输实际数据了。

问题:

1.为什么要三次握手,而不是两次握手?

解答:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送ack包。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。

采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认

2.什么是SYN攻击?

解答:在三次握手过程中,Server发送SYN-ACK之后,收到Client的ACK之前的TCP连接称为半连接(half-open connect),此时Server处于SYN_RCVD状态,当收到ACK后,Server转入ESTABLISHED状态。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server回复确认包,并等待Client的确认,由于源地址是不存在的,因此,Server需要不断重发直至超时,这些伪造的SYN包将产时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络堵塞甚至系统瘫痪。SYN攻击时一种典型的DDOS攻击,检测SYN攻击的方式非常简单,即当Server上有大量半连接状态且源IP地址是随机的,则可以断定遭到SYN攻击了。

四次挥手:

所谓四次挥手(Four-Way Wavehand)即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发,整个流程如下图所示:

由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭,上图描述的即是如此。

(1)第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。

(2)第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。

(3)第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。

(4)第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。

更为详细的四次挥手流程图:

四次挥手的一个例子:

上面是一方主动关闭,另一方被动关闭的情况,实际中还会出现同时发起主动关闭的情况,具体流程如下图:

问题:

为什么建立连接是三次握手,而关闭连接却是四次挥手呢?

解答:这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。

三次握手改成仅需要两次握手,死锁是可能发生:

考虑计算机A和B之间的通信,假定B给A发送一个连接请求分组,A收到了这个分组,并发送了确认应答分组。按照两次握手的协定,A认为连接已经成功地建立了,可以开始发送数据分组。可是,B在A的应答分组在传输中被丢失的情况下,将不知道A是否已准备好,不知道A建议什么样的序列号,B甚至怀疑A是否收到自己的连接请求分组。在这种情况下,B认为连接还未建立成功,将忽略A发来的任何数据分组,只等待连接确认应答分组。而A在发出的分组超时后,重复发送同样的分组。这样就形成了死锁

connect()函数

connect()函数最主要的功能就是建立客户与服务器的连接。客户调用connect()函数发起主动连接,TCP协议开始三次握手过程,三次握手完成后connect()函数返回。

函数原型:int connect(SOCKET s,const struct sockaddr * name,int namelen);

函数参数:

s:套接字描述符,客户机通过该套接字向服务器发送连接请求。

name:套接字s想要连接的主机地址和端口号

namelen:name缓冲区的长度

下面这段代码是客户端调用connect()函数与服务器建立连接的示例:

SOCKET sock_client;
struct sockaddr_in server_addr;        //用于存放要连接的服务器地址
int addr_len=sizeof(server_addr);      //地址结构占用的字节数

//填写服务器地址
memset((void*)&server,0,addr_len);
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(65432);     //服务器端口号为65432
server_addr.sin_addr.s_addr=inet_addr("127.0.0.1");  //服务器IP地址

//与服务器建立连接
if(connect(sock_client,(struct sockaddr *)&server_addr,addr_len)!=0){
	cout<"连接失败!错误代码:"<<WSAGetLastError()<<endl;
}

accept函数

accept()函数只适用于面向连接的套接字,并且跟listen()函数一样也是只能由服务器端调用,其功能是接收指定的监听套接字上传入一个连接请求,并尝试与请求方建立连接,连接建立成功则返回为该连接创建的新套接字描述符。

函数原型:SOCKET accept(SOCKET s,struct sockaddr * addr,int FAR* addrlen);

s:是一个套接字描述符,它应该处于监听状态。

addr:是一个sockaddr_in结构指针,包含一组客户端的端口号、IP地址等信息。

addrlen:用于接收参数addr的长度。

accept()函数返回一个已建立连接的新的套接字的描述符,即已连接套接字的描述符,服务器与本连接所对应客户端的所有后续通信,都应使用该套接字。原来的监听套接字仍然处于监听状态,可以继续接收其他客户的连接请求。

在默认情况下,如果调用accept()时还没有客户的连接请求到达,accept()将不会返回,进程将阻塞,直到有客户与服务器建立了连接才会返回。下面是accept()函数的使用示例,其中监听套接字sock_server已处于监听状态。

SOCKET newsock;                     //用保存accept()返回的套接字
struct sockaddr_in client_addr;     //用于存放客户端地址
int addr_len=sizeof(client_addr);   //地址结构占用的字节数
if(newsock=accept(client_addr,(struct sockaddr *)&client_addr,&addr_len))<0){
	cout<<"accept函数调用失败!错误代码:"<<WSAGetLastError()<<endl;
}else{
	//成功接收一个连接后要执行的代码
}

发送和接收数据

连接建立后,便可以在已建立连接的套接字上发送和接收数据了,对流式套接字,发送数据通常使用send()函数,而接收数据则通常使用recv()函数。

发送缓冲区和接收缓冲区

缓冲区是指内存中用于临时存放数据的一片连续的存储空间。使用send()函数发送数据时,应用程序必须实现申请一块内存空间作为缓冲区,并将要发送的数据存放该缓冲区中,这个缓冲区称为应用程序发送缓冲区,该缓冲区首地址是作为参数传递给send()函数的。

在使用recv()接收数据时,应用程序也必须事先申请一块内存空间用于存放从网络上收到的数据,该缓冲区被称为应用程序接收缓冲区,接收缓冲区的首地址和缓冲区的大小也是作为参数传递给recv()函数的。

除了应用程序中的发送缓冲区和接收缓冲区外,每个套接字也都有自己的发送缓冲区和接收缓冲区。套接字自己的收发缓冲区是创建套接字时由系统自动分配的,其默认大小为8KB,程序员可以根据调用setsockopt()函数来改变套接字内部收发缓冲区的大小。

除了send以外,任意一个Winsock函数在开始执行时,如果套接字的发送缓冲区中还有数据没有被发送出去,则该函数必须要先等待套接字的发送缓冲区中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该Winsock函数就返回SOCKET_ERROR。

实际上,当应用程序调用send()函数发送数据时,send()函数所做的工作仅仅是将要发送的数据从应用程序的发送缓冲区中复制到套接字的发送缓冲区,而调用recv()函数接收数据时,其所做的工作也仅仅是将套接字的接收缓冲区中存储的数据复制到应用程序的接收缓冲区中。实际的数据发送和接收工作都是由下层的TCP/IP协议自动完成的。

send()函数

函数原型:int send(SOCKET s,const char * buf,int len,int flags);

函数参数:

s:发送数据所使用的套接字的描述符,该套接字必须已建立连接;

buf:是存放要发送数据的缓冲区指针;

len:缓冲区buf中要发送的数据字节数;

flags:用于控制数据发送方式,通常取0,表示正常发送数据;

下面调用send()发送数据的例子,s是一个已建立连接的套接字:

char msgbuf[]="The message to be sent";
int size;
if((size=send(s,msgbuf,sizeof(msg),0))==SOCKET_ERROR){
	cout<<"发送信息失败!错误代码:"<<WSAGetLastError()<<endl;
}

send()函数的阻塞工作模式:

send()函数被调用后,它首先比较待发送的数据的长度len和套接字s的发送缓冲区的大小。如果len大于s的的发送缓冲区的长度,那么该函数返回SOCKET_ERROR;

如果len小于或者等于s的发送缓冲区的长度,那么send()先检查协议是否正在发送s的发送缓冲区中的数据,如果是,就阻塞,等待协议把数据发送完;

如果协议没有发送数据或者s的发送缓冲区中没有数据,那么send()就比较s的发送缓冲区的剩余空间和len的大小,如果len大于剩余空间字节数,send()就一直等待协议把s的发送缓冲区中的数据发送完;

如果len小于剩余空间字节数,send就仅仅把buf中的数据复制到剩余空间里。如果send()函数复制数据成功,就返回实际复制的字节数,如果send()在复制数据时出现错误,send()就返回SOCKET_ERROR。

注意:

并不是send()把s的发送缓冲区中的数据发送到连接的另一端,而是下层的网络协议自动发送的,send()仅仅是把buf中的数据复制到了s的发送缓冲区中。在一个TCP套接字上调用send()函数成功返回,仅仅表示数据由应用程序的发送缓冲区到套接字的发送缓冲区复制成功,并不代表对方已收到数据。如果后续的传送过程中出现了网络错误,那么下一个socket函数就会返回SOCKET_ERROR,所以说,即使send()函数成功返回,要发送的数据也存在不能被对方完全正确接收的可能。

send()函数的非阻塞工作模式:

在非阻塞模式下,send函数的工作仅仅是将数据复制到协议栈的缓冲区而已,如果缓冲区可用空间不够,则复制与缓冲区剩余空间字节数相同的数据,并返回成功复制的字节数,如果缓冲区可用空间为0,则返回-1,同时设置error为EAGAIN。也就是说,在非阻塞模式下,调用一次send()函数不一定能够将buffer中指定的len个字节数据全部发送出去,所以,在实际应用中必须改写这个函数,下面的函数TCPsend演示在非阻塞模式下正确发送数据的一种方法:

int TCPsend(SOCKET s,const char * buf,int len){
	int n=0,sendCount=0,length=len;
	if(buf==NULL) return 0;
	while(length>0){
		n=send(s,buf+sendCount,length,0);     //发送数据
		if(n==SOCKET_ERROR){
			cout<<"send()调用失败,错误码:",WSAGetLastError()<<endl;
			break;
		}
		length-=n;
		sendCount+=n;
	}
	return sendCount;                        //返回已发送的字节数
}

因此,在采用阻塞模式时,在网络不出错的情况下,调用一次send函数便可保证将指定的数据完全发送出去,而不必采用上面的方法。

recv()函数

函数原型:int recv(SOCKET s,char * buf,int len,int flags);

函数参数:

s:用于接收数据的套接字描述符,该套接字必须已经建立连接;

buf:用于接收数据的缓冲区指针;

len:是buf的长度;

flags:表示函数的调用方式,一般取值为0,表示接收正常数据。

下面是调用recv(),从已建立连接的套接字s上接收数据的例子:

char msgbuffer[1000];   //定义用于保存接收到的数据缓冲区
int size;
if((size=recv(s,msgbuffer,sizeof(msgbuffer),0))==SOCKET_ERROR)   //接收信息
{
	cout<<"接收信息失败!错误代码:"<<WSAGetLastError()<<endl;      //接收失败处理
}else{
	cout<<"The message from client:"<<msgbuffer<<endl;          //输出接收到的信息
}

recv()函数的阻塞工作模式:

当应用程序调用recv()函数时,如果套接字s的发送缓冲区还存在数据没有发送完成,则recv()先等待这些数据被协议发送完成,如果在传送这些数据时出现网络错误,那么recv()函数返回SOCKET_ERROR;

如果s的发送缓冲区中没有数据或者已有数据被成功发送完成后,recv先检查套接字s的接收缓冲区,如果s的接收缓冲区中有数据且协议已停止从网络接收数据,则recv函数就把s的接收缓冲区中的数据复制到buf指向的应用程序接收缓冲区中返回;

如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv函数就一直等待(阻塞),直到协议收到数据并接收完毕,recv()函数把s接收缓冲区中的数据复制到buf指向的应用程序接收缓冲区中返回。

每次调用成功,recv()函数返回实际复制的字节数。如果recv()在复制过程中出错,则返回SOCKET_ERROR;如果recv()函数在等待协议接收数据时网络中断,那么将返回0。

套接字接收缓冲区中的数据量可能大于buf指向的应用程序接收缓冲区的长度len,这时,recv()函数只能复制len个字节的数据,若要将套接字接收缓冲区中的数据全部读取,则需要多次调用recv()函数。因此,在发送端通过一次send()调用发送的数据在接收端可能需要多次调用recv()函数获取;

而接收端调用一次recv()函数获得的数据也有可能是发送端多次调用send()发送的数据。为了能够正确接收数据,在很多应用中,尤其是数据传送量较大的应用中,通常使用下面TCPrecv所采用的方法接收数据:

int TCPrecv(SOCKET schar * buf,int len){
	int nRev=0,recvCount=0,length=len;
	if(buf==NULL) return 0;
	while(length>0){              //循环接收数据
		nRev=recv(s,buf+recvCount,length,0);
		if(nRev==SOCKET_ERROR){   //网络出现异常
			cout<<"recv调用失败,错误码:",WSAGetLastError()<<endl;
			break;
		}
		length-=nRev;
		recvCount+=nRev;
	}
	return recvCount;             //返回实际接收到的字节数
}

关闭套接字

函数原型:int closesocket(SOCKET s);

函数参数:

s:要被关闭的套接字接口描述符。

应用程序完成通信任务后总是应该调用closesocket()以释放占用的资源,套接字一旦被关闭以后,应用程序就不能再使用这个套接字了,如果仍将它作为send()函数后recv()函数的参数,函数调用将会出错,对应的错误代码是WSAENOTSOCK,意思是给定的套接字描述符不是一个套接字。

Search

    Post Directory