Windows Sockets网络编程读书笔记(及简单C/S实现)

文章目录

简单介绍Socket

1 WindowsSockets 简介

Windows sockets(简称 Winsock) 是微软的窗口系统结构 (WOSA) 的一部分。它是起源于UNIX上的 Berkeley Software Distribution(BSD) 版本的套接字、并为 Windows 进行了专门地扩展。
Internet 是在 UNIX系统上发展起来的 ,在 UNIX 上有许多成熟的编程接口 ,其中最通用的是一种叫做 sockets(套接字) 的接口。套接字的实质是通信端点的一种抽象 ,它提供一种发送和接
收数据的机制。网络软件商为 Windows 开发一套标准的、通用的 TCP/ IP 编程接口 ,并使之类似于 UNIX下的 sockets ,这就是 Windows sockets ;Windows socket 的实现一般都由两部分组成 :开
发组件和运行组件。开发组件是供程序员开发 Winsock 应用程序使用的、它包括介绍 Winsock实现的文档、Winsock 应用程序接口 (API) 引入库和一些头文件。运行组件是 Winsock 应用程序接口的动态连接库(DLL) ,文件名为 Winsock. DLL ,应用程序在执行时通过装入它来实现网
络通信功能。
最初 ,Winsocket1. 1 版是专门为 Internet 设计的 ,现在的 2. x 版己经不再限于 Internet 和TCP/ IP 协议 ,它通过提供扩展的 API 编程接口 ,把自己的应用范围扩大到现存的和正在出现
的各种网络和协议 ,包括 PSTN、ISDN、无线网、所有的局域网协议、异步传输模式 ATM 等等 ;并且允许应用程序对所建立连接的可靠性、冗余度和带宽进行控制。由此可见 ,Winsock 有着广泛的应用。
Windows sockets 是 Windows 下网络编程的规范。这套规范是 Windows 下得到广泛应用的、开放的、支持多种协议的网络编程接口。它定义并记录了如何使用 API 与 Internet 协议族(IPs、通常我们指的是 TCP/ IP) 连接 ,尤其要指出的是所有的 Windows sockets 实现都支持流套接字和数据报套接字。当我们为客户机/ 服务器开发一个特殊的应用程序时 ,我们可以通过套接字来交换我们的数据结构和数据报 ,以完成应用程序之间的通信。应用程序调用 Winsock 的 API实现相互之间的通讯。Winsock 又利用下层的网络通讯协议功能和操作系统调用实现实际的通讯工作。
它们之间的关系如图 1 所示 :
Windows Sockets网络编程读书笔记(及简单C/S实现)

2 套接字的两种主要形式

在 Windows 套接字中 ,它主要有两种形式 :数据报套接字 (Datagram socket) 和流式套接字(Stream socket) 。流式(也称面向连接方式) 套接字采用的是 TCP 协议 ,它提供了双向的 ,有序的 ,无重复并且无记录边界的数据流服务。在这种方式下 ,两个通讯的应用程序之间先要建立一种虚拟的连接。流方式的特点是 :通讯可靠 ,对数据有校验和重发的机制 ,通常用来作数据文件的传输如 ftp ,telnet 等 ,适合于大量数据的传输。
数据报套接字采用的是UDP 协议 ,它建立在 IP 协议上 ,提供无连接数据报传输 ,支持双向的数据流 ,但并不保证是可靠、有序、无重复的。也就是说 ,一个从数据报套接字接收信息的进程有可能发现信息重复了 ,或者和发出时的顺序不同。数据报文方式由于取消了重发校验机制 ,能够达到较高的通讯速率 ,可以用作一些对数据可靠性要求不高的遮讯 ,如实时的语音、图象传输等。另外 ,数据报套接字支持广播发送 ,使用 setsockopt() 函数可以使指定的套接字发送广播消息。相比之下 ,流式套接字不支持广播发送。是使用流式套接字还是使用数据报套接字 ,对通信效率影响较大。在编程中 ,流式套接字
与数据报套接字是有区别的。在流式套接字中 ,服务器首先启动 ,遮过调用 socket () 建立一个套接字 ,然后调用 bind() 将该套接字和本地网络地址联系在一起 ,再调用 listen () 使套接字做好侦听的准备 ,并规定它的请求队列的长度 ,之后就调用 accept () 来接收连接。客户在建立套接字后就可调用 connect () 和服务器建立连接。连接一旦建立 ,客户机和服务器之间就可以调用 receive () 和 send () 来发送和接收数据。最后 ,待数据传送结束后 ,双方调用 close () 关闭套接字 ,如图 2 所示。

与流式套接字不同的是 ,在数据报套接字中 ,服务器不调用 accept ( ) ,客户机不调用connect() 。在发送数据之前 ,客户机和服务器之间尚未建立完整相关。无连接服务器通过sockct() 和 bind() 建立了本地半相关 ,在传输数据之前 ,无连接的两个端点已建立起来 ,分别以一个本地 socket 号和信宿端 socket 地址。于是 ,一个完整的相关在数据收发过程中动态地建
立起来 ,实现无连接客户和服务器彼此识别。如图 3 所示 :
Windows Sockets网络编程读书笔记(及简单C/S实现)

3 在 VC 下开发套接字程序

在实际编程中 ,我们一般采用面向对象技术 ,特别采用消息驱动机制实现多任务的
Windows 编程思想 ,VC 由于它的强大的功能而被广泛采用。

3. 1 在 VC 下开发套接字程序的基本步骤

在 VC + + 6. 0 中 ,我们可以用 Windows sockets API 来编写网络程序 ,其特点是十分灵活 ,可以充分利用众多的 Windows sockets API 函数 ,相比之下编程比较烦琐。另外我们可以使用MFC 封装的 CAsyncSocket 和 CSocket 两个类来进行网络编程 ,它把与套接字有关的 Windows 消
息转换为回调函数。CAsyncSocket 类比 CSocket 更加面向低层 ,它提供的低级接口几乎与WinSocket API 调用直接对应 ,使用比较灵活 ,但它对编程人员的要求也高 ,需要对网络了解得更多。CSocket 是 CAsyncSocket 的导出类 ,通过 MFC 中的 CArchive 类的对象提供了更高层次的抽象 ,它封装了 socket 实现中的许多细节 ,并将 socket 与 CArchive 相结合 ,使用它与使用 MFC中的文档串行化协议相类似 ,使用便利。CSocket 编程的主要步骤如下 :
(1) 构造套接字对象。
(2) 使用该对象构造基本的套接字。对于 CSocket 客户端对象 ,使用缺省参数 Creat ;对于CSockct 服务器对象 ,应指明一个端口号作为 Create 的一个参数 ,用于监听。
(3) 建立客户端 CSocket ,调用 CAsyncSocket : :Connect() 建立与服务器端的连接。服务器端
套接字调用 CAsyncSocket : :Listen() 监听 ,并在收到客户端请求后调用 CAsyncSocket : :Accept() 。
(4) 构造 CSocketFile 对象 ,并使 Csocket 对象与之关联。
(5) 构造 CArchive 对象 ,用于接收或发送数据。
(6) 使用 CArchive 对象来进行客户端与服务器端的套接字通信。
(7) 删除 CArchive、CSocketFile、CSocket 对象。流程图如图 4 所示。
Windows Sockets网络编程读书笔记(及简单C/S实现)

3. 2 回调函数的使用

为了使网络通信更加方便 ,CAsyncSocket 和 CSocket 提供了一些回调函数。主窗口通过调用这些回调函数来通知套接字的一些重要事件的来临。这些回调函数有 OnReceive、OnSend、OnConnect、OnAccept、OnClose ,它们可以通过在两个类中重载得到。这两个类仅仅是通过回调函数将消息转化为通知 ,具体的如何响应这些通知 ,还须我们自己来实现。
Void CReceSocket : :OnReceive (int nErrorCode)
CASyncSocket : :OnReceive (nErrorCode) ;
Receive (1pBuf ,int nBufLen ,int nFlags) ;
如果自己的类是 CAsyncSocket 继承而来 ,为了使通信更加便利 ,必须重载这些回调函数。如果自己的类是继承 CSocket ,将由你根据情况自己决定是否重载它们。必须指出的是 ,CSocket对象从不调用 OnSend 和 OnConnect 这两个通知函数 ,而只能调用 Send 函数来发送数据 ,直到发送完所有数据 Send 才返回。同样只能调用 Connect 函数来进行连接 ,但调用 Connect 时会发生阻塞 ,直到成功地建立了连接或有错误发生。如果使用多线程 ,调用 Connect 的线程在Connect () 发生阻塞时仍能处理 Windows 消息。

3. 3 利用多线程技术来开发网络通信

CSockct 类的缺省方式为阻塞方式 ,为了避免阻塞的种种缺点 ,可以使用多线程技术。我们可以在一个工作线程中处理数据的接收和发送 ,该工作线程可以在后台运行 ,套接字在工作线程中的阻塞不会影响主线程中的其它活动 ,这样主线程可以处理主窗口的消息映射。除了使用工作线程外 ,我们还可以使用用户界面线程来实现 ;用户界面线程增加了消息映射 ,在下面的例子中将会用到。而以 syncSocket 类的缺省方式为非阻塞方式。在异步方式中 ,调用会立即返回 ,用 GetLastError 函数会获相应的错误代码为 WSAEWOULDBLOCK,表示无连接可以接
受。举例说明 ,在异步方式中 ,在调用 Receive 函数后 ,会得到 WSAEWOULDBLOCK的错误信
息 ,直到 OnReceive 回调函数被调用以通知我们可以再次接收数据了。

3. 4 使用多线程进行套接字编程时应注意同步问题

在使用多线程技术进行网络编程时 ,必须注意套接字对象的同步问题 ,可以使用线程同步机制来协调套接字对象的存取。对套接字调用时 ,如果不进行同步将可能会导致不可预测的结果。例如 ,如果有两个线程同时调用同一套接字进行 send ,那么数据发送的先后顺序就无法保证了。另外 ,如果两个线程中调用同一个套接字 ;在一个线程中关闭一个末完成的阻塞的套
接字将会导致另一个线程使用同一套接字的阻塞调用出错(WSAEINTER) 返回 ,就象操作被取消一样。在 Windows 下 ,在两个线程中传递套接字对象是不安全的。对于套接字编程来说 ,一个套接字对象应该仅仅用于单个线程 ,在两个线程之间不能传递套接字对象。
例如服务器一般可以接受多个连接 ,它每接受一个连接 ,就创建一个线程用来处理连接。为达到这个目的 ,仅仅在这两个线程个传递套接字对象是不够的。
虽然在两个线程之间不能传递套接字对象 ,但我们可以在线程之间传递线程句柄。因此我们可以按以下步骤进行 :
(1) 把附加在接受连接的线程的套接字对象上的套接字句柄分离出来。
(2) 在两个线程中传递套接字句柄。
(3) 在处理套接字连接的线程中 ,把这个套接字句柄附加到套接字对象上。
为了更好的说明以上方法 ,现将程序的部分列出 :

/ / 在主线程中创建监听线程的 OnAccept 函数
Void ClistenSocket : :OnAccept(int nErrorCode) {
CAsyncSocket soc :/ / 用于接受连接请求而建的临时对象
Accept (soc) : / / 接受请求
/ / 创建新线程并挂起(该线程是用户界面线程)
CSockThread 3 pThread = ( (CSockThread 3 ) AfxBeginThread(RUNTIMECLASS(CsockThread) ,
THREAD PRI0RITY NORMAL、O ,CREATE SUSPENDED) ;
/ / 将套接字句柄从套接字对象中分离出来 ,并保存
pThread 一 > m hSocket = (SOCKET) soc. Detach() ;
/ / 开始执行新线程
pThread 一 > ResumeThread() ;
CAsyncSocket : :OnAccept(nErrorCode) ; }
/ / 处理连接线程(即新线程) 的 1nitInstance 函数 ,
BOO CSockThread : :1nitInstance ()
{
/ / 把这个套接字句柄附加到新的套接字对象上 ,
/ / 这样主线程的通知函数就能发送到新线程中 ,在新线程中处理
m socket. Attach(m-hSocket) ;
return TRUE :
}

通过以上步骤 ,我们就可以利用多线程技术来进行网络通信了。

代码实现

客户端

#include <winsock2.h>						//包含头文件  
#include  <stdio.h>
#include  <windows.h>
#pragma comment(lib,"WS2_32.lib")			 //显式连接套接字库  

int main()									 //主函数开始  
{
	WSADATA data;							//定义WSADATA结构体对象  
	WORD w = MAKEWORD(2, 0);				//定义版本号码  
	::WSAStartup(w, &data);					//初始化套接字库  
	SOCKET s;								//定义连接套接字句柄  
	char sztext[10] = { 0 };
	s = ::socket(AF_INET, SOCK_STREAM, 0);	//创建TCP套接字  
	sockaddr_in addr;						//定义套接字地址结构  
	addr.sin_family = AF_INET;				//初始化地址结构  
	addr.sin_port = htons(75);
	addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	printf("客户端已经启动\r\n");			//输出提示信息  
	::connect(s, (sockaddr*)&addr, sizeof(addr));
	::recv(s, sztext, sizeof(sztext), 0);
	printf("%s\r\n", sztext);
	::closesocket(s);						//关闭套接字句柄  
	::WSACleanup();							//释放套接字库  
	if (getchar())							//如果有输入,则关闭程序  
	{
		return 0;							//正常结束程序  
	}
	else
	{
		::Sleep(100);						//程序睡眠  
	}
}

服务端

//socket网络操作的头文件
//Winsock 是由Unix下的BSD Socket发展而来,是一个与网络协议无关的编程接口
#include <winsock2.h>                       //包含头文件  
#include  <stdio.h>
#include  <windows.h>
//socket网络操作的静态库
#pragma comment(lib,"WS2_32.lib")           //套接字库    显式连接

int main()                                  //主函数开始  
{
	//Windows Sockets API	WSADATA	   data数据  
	//Windows Sockets Asynchronous  WSA Windows异步套接字数据
	//这个结构体对象专门用来存储WSAStartup中的返回值
	//存放windows socket初始化信息
	 //定义WSADATA结构体对象 
	WSADATA data;                          
	//定义版本号码  
	WORD w = MAKEWORD(2, 0);                   
	//链接成功后需要的提示--定义并初始化发送到客户端的字符数组  
	char sztext[] = "beidou";       
	
	//准备完事之后
	//进行服务器端的创建需要步骤:
	//1:初始化套接字
	//2:初始化地址结构
	//3:绑定套接字
	//4:监听等待链接
	//5:关闭并且释放套接字资源

	//1:初始化套接字
	//我们为了使用Windows Sockets API 提供的函数  那么久必须使用WSAStartup函数对Winsock服务的初始化
	::WSAStartup(w, &data);//初始化套接字库  
	//声明两个句柄,连接套接字和数据收发套接字句柄  
	SOCKET socket_1, socket_2;//socket_1用来作为链接套接字   socket_2用来进行收发
	//创建TCP套接字
	/*
	参数1:
	AF_UNIX(本机通信)
	AF_INET(TCP/IP – IPv4)
	AF_INET6(TCP/IP – IPv6)
	参数2:
	SOCK_STREAM 提供有序的、可靠的、双向的和基于连接的字节流,使用带外数据传送机制,TCP。
	SOCK_DGRAM 支持无连接的、不可靠的和使用固定大小(通常很小)缓冲区的数据报服务,UDP。
	SOCK_RAW(原始套接字)
	参数3:
	一般情况下都是0 确认套接字使用的协议族和类型以后为0  不确定的时候才借用这个参数进行定义,这里不讲解
	*/
	socket_1 = ::socket(AF_INET, SOCK_STREAM, 0);  
	//声明套接字地址结构  一个链接使用一个收发使用
	sockaddr_in addr, addr2;
	//定义套接字地址结构大小  
	int n = sizeof(addr2);
	//初始化地址结构  
	//1:确定协议族为 INET
	addr.sin_family = AF_INET;
	//2:误区:不是端口号,指:网络字节序,网络字节序一般是大端字节序
	//例如:端口6000的网络字节序是28695   可以写成 htons(6000) 也可以写成addr.sin_port =28695
	addr.sin_port = htons(75);
	//给IP地址
	//INADDR_ANY 0.0.0.0  泛指localhost,本机的所有地址(多网卡的情况下)
	addr.sin_addr.S_un.S_addr = INADDR_ANY;
	//套接字有了,IP地址有了
	::bind(socket_1, (sockaddr*)&addr, sizeof(addr));    //绑定套接字  
	::listen(socket_1, 5);                              //监听套接字  


	printf("服务器已经启动\r\n");              //输出提示信息  


	while (true)
	{
	
		socket_2 = ::accept(socket_1, (sockaddr*)&addr2, &n);    //接受连接请求  
		
		if (socket_2 != NULL)
		
		{
		
			printf("%s已经连接上\r\n", inet_ntoa(addr2.sin_addr));
			
			::send(socket_2, sztext, sizeof(sztext), 0); //向客户端发送字符数组  
		}

		
		::closesocket(socket_1);                       //关闭套接字句柄  
		
		::closesocket(socket_2);
		
		::WSACleanup();                         //释放套接字库  
		
		if (getchar())                           //如果有输入,则关闭程序  
		
		{
		
			return 0;                           //正常结束程序  
		}

		else
		{
			::Sleep(100);                       //应用睡眠0.1秒  
		}
	}
}
上一篇:Java控制IP TTL?


下一篇:Go语言---基于TCP的Sockets编程