Winsocket 一:单线程阻塞server&client程序(tcp)

简单介绍什么是Winsocket以及server/client应用程序模型,给了一个简单的server/client程序的tcp实现。

一、Winsocket简介

        Winsocket是unix/linux下的berkeley socket在Windows下的实现。unix/linux下的berkeley socket是网络通讯方面的基石,应用程序通过调用berkeley socket的API进行相互通讯,berkeley socket则利用具体的网络通讯协议和操作系统的调用来为我们完成具体的通讯工作。Winsocket保留了berkeley socket的所有内容,并且为了其能在Win32消息机制和多线程的环境下更好的工作。Winsocket在berkeley socket原有的基础上对其进行了扩充。如我们可以利用WSAAsyncSelect对Socket消息进行订阅,以及使用WSAGetLastError对多线程环境下的Winsocket错误进行捕获

二、server/client应用程序模型

        服务器程序一直处于监听的状态,等待客户端程序的连接。客户端程序像服务器程序发送连接请求,服务器程序接受该连接请求,同时与客户端程序建立连接。此时客户端程序就可以向服务器发送具体的请求,获取相关的数据。服务器\客户端模型有三种连接方式,一种是面向连接的(TCP),面向连接的服务是一种可靠的服务,它通过数据流进行数据的传输,面向连接的服务实现了无差错无重复的顺寻数据发送。一种是面向无连接的(UDP),面向无连接的服务是一种不可靠的服务,它通过数据报进行数据传输,由于数据报进行传输时的顺序是无序的,所以它是不可靠的服务。最后一种是多播的方式,及服务器程序主动向多个客户端程序发送信息。

        面向连接的服务器\客户端应用程序模型的程序流程图如下所示:

Winsocket 一:单线程阻塞server&client程序(tcp)_第1张图片

        在此模型的阻塞模式中,服务端程序在执行accept操作、客户端程序connect操作、以及服务端\客户端在进行read和write操作时,如果这些操作既没有成功也没有失败,应用程序会在执行这些操作的地方一直阻塞着。

三、基于TCP通信的Server/Client程序实例

1、服务器端源代码

<span style="font-family:Verdana;">#include <iostream>
#include <cassert>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")

#define ASSERT assert

static const int c_iPort = 10001;

using std::cout;
using std::endl;

int _tmain(int argc, _TCHAR* argv[])
{
	int iRet = SOCKET_ERROR;

	//初始化Winsocket
	WSADATA data;
	ZeroMemory(&data, sizeof(WSADATA));
	iRet = WSAStartup(MAKEWORD(2, 0), &data);
	ASSERT(SOCKET_ERROR != iRet);

	//建立服务器端程序的监听套接字
	SOCKET skListen = INVALID_SOCKET;
	skListen = socket(AF_INET, SOCK_STREAM, 0);
	ASSERT(INVALID_SOCKET != skListen);

	 //初始化监听套接字地址信息,网络格式
	sockaddr_in adrServ;
	ZeroMemory(&adrServ, sizeof(sockaddr_in));
	adrServ.sin_family	= AF_INET;	 //初始化地址格式
	adrServ.sin_port	= htons(c_iPort);   //网络字节顺序和主机字节顺序相反,使用htons将主机字节顺序转换成网络字节顺序
	adrServ.sin_addr.s_addr	 = INADDR_ANY;	//任意IP

	//绑定监听套接字到本地
	iRet = bind(skListen, (sockaddr*)&adrServ, sizeof(sockaddr_in));
	ASSERT(SOCKET_ERROR != iRet);

	//使用套接字进行监听
	iRet = listen(skListen, SOMAXCONN);
	ASSERT(SOCKET_ERROR != iRet);

	for (;;)
	{
		//客户端向服务器端发送连接请求,服务器端接受客户端的连接
		SOCKET skAccept = INVALID_SOCKET;
		sockaddr_in adrClit;
		ZeroMemory(&adrClit, sizeof(sockaddr_in));
		int iLen = sizeof(sockaddr_in);
		skAccept = accept(skListen, (sockaddr*)&adrClit, &iLen);// 如果没有客户端程序请求连接,服务端程序会一直阻塞在这里等待连接
		ASSERT(INVALID_SOCKET != skAccept);
		cout << "New connection " << skAccept << ", c_ip: " << inet_ntoa(adrClit.sin_addr) << ", c_port: " << ntohs(adrClit.sin_port) << endl;

		const int c_iBufLen = 512;
		char recvBuf[c_iBufLen + 1] = {'\0'};
		const char c_prefix[] = "Server recv:";
		char sendBuf[c_iBufLen + 16 + 1] = {'\0'} ;
		strcpy(sendBuf, c_prefix);

		for (;;)
		{
			//接收客户端消息
			iRet = recv(skAccept, recvBuf, c_iBufLen, 0);// 接收客户端发送的信息, 如果客户端不发送信息,则线程会阻塞到此处
			if(iRet == 0)//客户端优雅的关闭了此连接
			{
				cout <<	"Connection " << skAccept << " shutdown." << endl;
				break;
			}
			else if(SOCKET_ERROR == iRet)
			{
				cout << "Connection " << skAccept << " recv error." << endl;
				break;
			}
			recvBuf[iRet] = '\0';
			cout << "Connection " << skAccept << " recv: " << recvBuf << endl;

			//向客户端发送消息
			strcpy(sendBuf + strlen(c_prefix), recvBuf);
			iRet = send(skAccept, sendBuf, strlen(sendBuf), 0);	 // 客户端如果没有足够的缓冲区接受信息,则线程会阻塞到此处   
			if(SOCKET_ERROR == iRet)
			{
				cout << "Connection " << skAccept << "send error." << endl;
				break;
			}
		}

		//关闭该套接字端口
		iRet = shutdown(skAccept, SD_SEND);
		while (recv(skAccept, recvBuf, c_iBufLen, 0) > 0);
		ASSERT(SOCKET_ERROR != iRet);

		//清理该套接字的资源
		iRet = closesocket(skAccept);
		ASSERT(SOCKET_ERROR != iRet);

		//break;
	}

	// 关闭该套接字的连接   
	iRet = shutdown(skListen, SD_SEND);
	ASSERT(SOCKET_ERROR != iRet);

	// 清理该套接字的资源   
	iRet = closesocket(skListen);   
	ASSERT(SOCKET_ERROR != iRet); 

	// 清理Winsocket资源,<span style="white-space: pre;">对Winsocket和ws2_32.dll的清理</span>  
	iRet = WSACleanup();   
	ASSERT(SOCKET_ERROR != iRet);

	system("pause");
	return 0;
}</span>
代码解释:
	WSADATA data;
	ZeroMemory(&data, sizeof(WSADATA));
	iRet = WSAStartup(MAKEWORD(2, 0), &data);
	ASSERT(SOCKET_ERROR != iRet);
       进程初始化Winsocket和Ws2_32.dll,使后面的函数调用有效。 WSAStartup第一个参数为要使用的Winsocket的版本,第二个参数在WSAStartup初始化后,可以获得一些Winsocket相关信息,如该版本Winsocket所支持的最大socket数量以及UDP包的最大大小。

	SOCKET skListen = INVALID_SOCKET;
	skListen = socket(AF_INET, SOCK_STREAM, 0);
	ASSERT(INVALID_SOCKET != skListen);
       socket函数分配相应的资源并将该socket绑定到一个特定的传输服务提供者。socket的第一个参数为网络地址族,该参数只能为AF_INET,第二个参数可以为SOCK_STREAM或者SOCK_DGRAM。SOCK_STREAM为一个流式套接口,它提供双向可靠、面向连接的TCP服务。SOCK_DGRAM为一个数据报套接口,它提供不可靠、面向无连接的UDP服务。第三个参数一般选择为0,表示由Winsocket选择具体的协议使用。

	
	iRet = shutdown(skAccept, SD_SEND);<span style="font-family:Arial, Helvetica, sans-serif;">//关闭该套接字端口</span>
	while (recv(skAccept, recvBuf, c_iBufLen, 0) > 0);
	ASSERT(SOCKET_ERROR != iRet);
	iRet = closesocket(skAccept);  <span style="font-family:Arial, Helvetica, sans-serif;">//清理该套接字的资源</span>
	ASSERT(SOCKET_ERROR != iRet);
       优雅关闭:在客户端关闭该套接口或者出现接收发送数据错误的时候,都应该关闭该套接口。但在调用关闭套接口的closesocket函数之前,我们应该先调用shutdown函数,以使对方可以收到该套接口已经关闭的信息。在调用shutdown函数之后,我们应该使用recv函数读取在队列之中仍未读完的数据,最后我们就可以使用closesocket函数关闭该套接口了。

2、客户端源代码

<span style="font-family:Verdana;">#include <iostream>
#include <cassert>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")

#define ASSERT assert

static const char c_szIP[] = "127.0.0.1";
static const int c_iPort = 10001;

using std::cin;
using std::cout;
using std::endl;

int _tmain(int argc, _TCHAR* argv[])
{
	int iRet = SOCKET_ERROR; 
   
	WSADATA data;   
	ZeroMemory(&data, sizeof(WSADATA));   
	iRet = WSAStartup(MAKEWORD(2, 0), &data);   
	ASSERT(SOCKET_ERROR != iRet);

  
	SOCKET skClient = INVALID_SOCKET;   
	skClient = socket(AF_INET, SOCK_STREAM, 0);   
	ASSERT(INVALID_SOCKET != skClient); 
   
	sockaddr_in adrServ;                             
	ZeroMemory(&adrServ, sizeof(sockaddr_in));         
	adrServ.sin_family      = AF_INET;                
	adrServ.sin_port        = htons(c_iPort);  
	adrServ.sin_addr.s_addr = inet_addr(c_szIP);   
	
	// 使用连接套接字进行连接   
	iRet = connect(skClient, (sockaddr*)&adrServ, sizeof(sockaddr_in));   
	ASSERT(SOCKET_ERROR != iRet);   
	const int c_iBufLen = 512;   
	char szBuf[c_iBufLen + 16 + 1] = {'\0'};

	for(;;)   
	{   
		cout << "what you will say:";   
		cin  >> szBuf;   
		if(0 == strcmp("exit", szBuf))   
		{   
			break;   
		}   
		// 向服务器端发送信息   
		iRet = send(skClient, szBuf, strlen(szBuf), 0); // 服务器端如果没有足够的缓冲区接受信息,则线程会阻塞到此处   
		if(SOCKET_ERROR == iRet)   
		{   
			cout << "send error." << endl;   
			break;   
		}  

		// 接收服务器端发送的信息   
		iRet = recv(skClient, szBuf, c_iBufLen, 0); // 如果服务器端没有发送数据,则会阻塞到此处   
		if(0 == iRet)   
		{   
			cout << "connection shutdown." << endl;   
			break;   
		}   
		else if(SOCKET_ERROR == iRet)   
		{   
			cout << "recv error." << endl;   
			break;   
		}   
		szBuf[iRet] = '\0';   
		cout << szBuf << endl;   
	}

	// 关闭该套接口   
	iRet = shutdown(skClient, SD_SEND);   
	while(recv(skClient, szBuf, c_iBufLen, 0) > 0);   
	ASSERT(SOCKET_ERROR != iRet); 

	// 清理该套接口的资源   
	iRet = closesocket(skClient);   
	ASSERT(SOCKET_ERROR != iRet); 

	// 清理Winsocket资源   
	iRet = WSACleanup();   
	ASSERT(SOCKET_ERROR != iRet);   
	system("pause");
	return 0;
}</span>
       上面的服务器程序一次只能与一个客户端通信。应该在服务端应用程序的主线程中不停的调用accept操作,以使服务端程序能不停地接受客户端程序发送过来的连接请求。而在接受了一个客户端的连接请求后,我们应改为每一个接受的连接请求开辟一个专门的线程来接受客户端程序发送的请求以及为具体的请求返回特定的信息.
详见链接: Winsocket 二:多线程阻塞服务器程序(tcp)

<span style="font-family:Verdana;"><span style="font-family:Verdana;">        </span>参考链接: </span><span style="font-family:Arial, Helvetica, sans-serif;BACKGROUND-COLOR: rgb(255,255,255)"><a target=_blank target="_blank" href="http://www.doc88.com/p-905997530710.html">http://www.doc88.com/p-905997530710.html</a></span>

你可能感兴趣的:(server,tcp,socket,通信)