写这篇日志,并不是要记录令人眼前一亮的算法,只是为了本人健忘的脑袋做一点准备。
要进行网络通信编程,就要用到socket(套接字),下面以TCP为例展示如何利用socket通信。
要进行socket编程,首先要为工程链接导入库文件 ws2_32.lib ,然后添加头文件 #include <Winsock2.h> ,然后在App类的InitInstance()函数里面加载套接字库,加载套接字库的代码可查看MSDN里WSAStartup函数页面下端example的代码,在加载套接字库的代码里面有一句wVersionRequested = MAKEWORD( 2, 2 );这句是指定采用2.2版本的套接字库,可根据需要修改为其他版本的套接字库。
1.TCP下的socket通信:
TCP是面向链接的通信,通信的socket双方中必须有一个是服务器端socket,另一端是客户端socket。下面用代码来展示服务器端socket和客户端socket是如何建立链接并通信的。
服务器端连接过程:
1. 使用socket函数创建一个服务器端socket:
SOCKET sockSrv=socket(AF_INET,SOCK_STREAM,0);
如上用socket函数创建了一个名字叫sockSrv的socket,socket函数的第二个参数指定了这个是什么类型的socket,如果是TCP类型的socket则为SOCK_STREAM,如果是UDP类型的socket则为SOCK_DGRAM。
2.创建一个地址结构体,为地址结构体指定地址,然后使用bind函数把socket和地址绑定:
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
addrSrv.sin_family=AF_INET;
addrSrv.sin_port=htons(6000);
bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
如上,创建了一个名字叫addrSrv的地址结构体,addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
是指定IP地址为本机的IP地址,addrSrv.sin_port=htons(6000);是指定端口为6000端口,一定要使用1024以上的端口号,1024以下的端口后是系统保留的。在这里有两个函数htonl和htons,这两个函数的作用以后再说。然后使用bind函数把sockSrvt和addrSrv绑定起来。
3.设置socket为监听模式:
listen(sockSrv,5);
使用listen函数把sockSrv设为监听模式,第二个参数是等待连接队列的最大长度。如果设置为SOMAXCONN,那么将这个套接字设置为最大的合理值。这个值不是在一个端口上同时可以进行连接的数目,例如:如果把参数设置为2,当有3个连接请求同时到来时,前两个连接请求被放到等待请求连接队列中,然后程序依次为这些请求服务,而第三个连接请求就被拒绝了。对多个连接请求的处理不是同时进行的,必须完成请求连接队列中一个连接请求的连接,才能开始进行请求连接队列中下一个连接请求的连接。
4.等待客户端的连接请求到来:
SOCKADDR_IN addrClient;
int len=sizeof(SOCKADDR);
SOCKET sockConn=accept (sockSrv,(SOCKADDR*) &addrClient ,&len);
首先创建一个地址结构体addrClient ,当有客户端请求连接sockSrv,accept 函数就会执行,建立连接并把客户端的IP地址和端口信息记录到地址结构体addrClient 里。必须注意到,accept函数的返回值是一个socket,在上面的代码中是SOCKET sockConn,这个名字叫sockConn(名字可由程序员随便取)的socket有什么用呢?其实当成功完成连接后,与客户端socket连接的是sockConn,而不是sockSrv,以后与客户端socket进行数据传送的socket也是sockConn,而不是sockSrv,简单的说,服务器端socket sockSrv只负责接收连接请求和进行连接操作,当连接操作完成后,与客户端socket连接的是accpet函数返回的socket,以后与客户端socket进行数据传输的也是这个accpet函数返回的socket。
再来看TCP客户端是怎样发起连接请求的。
客户端编程也是用到socket,因此链接导入库文件,包含头文件,加载套接字库也必须先在客户端进行。
客户端请求连接过程:
1.使用socket函数创建一个客户端socket:
SOCKET sockClient=socket(AF_INET,SOCK_STREAM,0);
2.向服务器端socket发出连接请求:
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
addrSrv.sin_family=AF_INET;
addrSrv.sin_port=htons(6000);
connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
上面创建了一个名字叫addrSrv的地址结构体,然后把服务器端socket的IP地址(假设服务器端IP地址是127.0.0.1)和端口来设置这个地址结构体,然后通过connect函数向服务器端socket发出连接请求。在设置addrSrv地址结构体的IP地址时用了一个inet_addr函数,这个函数的作用以后再说。
由此可见,客户端socket并不需要绑定IP地址和端口,为什么服务器端socket需要绑定而客户端socket不用绑定呢?如果服务器端socket不绑定IP和端口,客户端socket又哪知道向哪个IP地址和端口发起连接请求呢?因此服务器端socket是必须绑定IP端口的。而客户端socket不需要用bind函数绑定端口,系统会自动为socket绑定一个随机的端口,服务器只要用accept函数返回的socket与客户端socket交换数据就行了,如果服务器需要查询客户端socket的IP和端口,可以查看accept函数的第二个参数记录的客户端socket地址信息。
连接的操作在这里完成了,然后是进行数据的传输:
发送数据使用send函数:
int send(
SOCKET s, //要发送数据的socket,例如设置为服务器端的sockConn,则数据会发送到
与sockConn相连接的客户端socket sockClient上去。相反若设置为sockClient,
则数据发送到与sockClient相连接的服务器端socket sockConn上去。
const char FAR *buf, //要发送数据buf的地址。
int len, //要发送数据buf的长度
int flags //一般设置为0即可。
);
接收数据使用recv函数:
int recv(
SOCKET s, //要接收数据的socket,例如设置为服务器端的sockConn,则接收与sockConn相
连接的客户端socket sockClient发送的数据。相反若设置为sockClient,则接收
与sockClient相连接的服务器端socket sockConn发送的数据。
char FAR *buf, //要发送数据buf的地址。
int len, //要接收数据buf的长度
int flags //一般设置为0即可。
);
socket使用完毕后调用closesocket()函数关闭一个socket以回收资源。在程序关闭之前,必须调用WSACleanup函数终止对套接字库的使用,注意必须在App类(应用程序类)的析构函数中调用WSACleanup函数。
接上一篇,建立连接后,服务器端的sockConn与客户端的sockClient就连接起来并且可以互相传输数据了,使用closesocket函数关闭一个socket,socket被关闭,连接也就断开了。下面是断开连接后的各种情况。
情况1:关闭服务器端sockConn--closesocket(sockConn)之后
关闭服务器端sockConn后,对sockConn使用recv函数接收数据,recv函数会马上返回SOCKET_ERROR,对sockConn使用send函数发送数据,send函数也会马上返回SOCKET_ERROR。
之后对客户端sockClient的情况分析就有点复杂了:
1.sockClient使用send函数发送数据。第一次send函数能成功返回发送数据的大小,并不会返回SOCKET_ERROR 。虽然sockClient成功send了数据,但sockConn是无法接收到的。但sockClient使用send函数发送数据成功仅限于第一次send,之后使用send函数返回的都是SOCKET_ERROR 。
2.sockClient使用recv函数接收数据。如果sockClient使用recv函数之前没有使用过send函数,那么recv函数的返回值总是0(感觉好像很奇怪,recv函数返回0,难道还有成功接收到0个字节的数据这种说法?不明白)。直到sockClient调用过send函数之后,recv函数的返回值总是SOCKET_ERROR。
情况2:关闭客户端sockClient--closesocket(sockClient)之后
这时结果就和情况1相反:
关闭客户端sockClient后,对sockClient使用recv函数接收数据,recv函数会马上返回SOCKET_ERROR,对sockClient使用send函数发送数据,send函数也会马上返回SOCKET_ERROR。
之后对服务器端sockConn的情况分析也一样:
1.sockConn使用send函数发送数据。第一次send函数能成功返回发送数据的大小,并不会返回SOCKET_ERROR 。虽然sockConn成功send了数据,但sockClient是无法接收到的。但sockConn使用send函数发送数据成功仅限于第一次send,之后使用send函数返回的都是SOCKET_ERROR 。
2.sockConn使用recv函数接收数据。如果sockConn使用recv函数之前没有使用过send函数,那么recv函数总是返回0。直到sockConn调用过send函数之后,recv函数的返回值总是SOCKET_ERROR。
知道上面两个断开连接的情况后就要考虑客户端和服务器端如何协调关闭连接。TCP连接的双方Asocket和Bsocket,Asocket想要关闭连接,Asocket的计算机除了closesocket(Asocket)之外,还要另外通知Bsocket的计算机连接已经断开了,Bsocket的计算机收到通知后closesocket(Bsocket)来回收资源,避免Bsocket还在哪里傻傻的接收/发送数据,如果不想这样明显通知Bsocket的计算机连接已断开,Bsocket也可以尝试自己判断,如果Bsocket多次调用send函数总是返回SOCKET_ERROR或者Bsocket多次调用recv函数总是返回0或SOCKET_ERROR,那就要意识到连接很可能已经断开了。
如果数据的流向是单向的,例如说数据只从Asocket流向Bsocket(类似文件传输就是这样),那么Asocket只会调用send函数,而Bsocket只会调用recv函数,这时候如果其中一方要停止数据的传输,就会有两种情况出现:
1.如果Asocket的计算机不想发送数据而closesocket(Asocket),由于Bsocket从来不调用send函数,因此Bsocket的recv函数总是返回0,那么Bsocket的计算机就要意识到Asocket很可能已经关闭了,让Bsocket的计算机closesocket(Bsocket)。
2.如果Bsocket的计算机不想接收数据而closesocket(Bsocket),这时Asocket继续调用send函数发送数据,第一次send还是成功的,但从第二次send开始就总会返回SOCKET_ERROR,但是Asocket的计算机无法判断send函数返回SOCKET_ERROR是由Bsocket关闭造成的还是Asocket关闭造成的(因为Asocket关闭后Asocket调用send函数也是返回SOCKET_ERROR),因此Asocket的计算机无法判断Asocket关闭了没有,简单的解决方法是如果Bsocket的计算机不想接收数据,先不要关闭Bsocket,而是发通知给Asocket的计算机告诉它我不想收数据了,Asocket的计算机收到通知后关闭Asockt,这样情形就回到上面情况1去了,而且也知道Asocket调用send函数返回SOCKET_ERROR肯定是由于Asockt关闭造成的而不是由Bsocket关闭造成的。