事件驱动套接字流:SocketLib。






4.86/5 (14投票s)
一个C++ TCP套接字流库。
引言
SocketLib是一个跨平台的、基于事件的、半异步的流库。它源自标准的IO流。包含SocketLib头文件不会包含任何WinSock、Windows或BSD套接字头文件。因此,您的代码将不会有任何可能引起问题的C宏。此外,该库由多个文件组成,打包在一起,不需要放在包含路径中。在Linux上唯一的依赖是pThread,在Windows上则是pThread和WinSock2库。
背景
需要具备网络和C++流的基础知识。
Using the Code
SocketLib是一个基于事件的、半异步的套接字流。它源自标准的C++套接字,因此可以使用所有的提取器(>>)和插入器(<<)。半异步方法允许程序员定义一个事件处理程序来异步处理传入数据,同时又不失去阻塞读取数据的能力。该系统的主要目标是降低套接字编程的难度,并使套接字系统更加C++友好。第二个目标是使系统足够小,以便集成到任何项目中。事实上,有许多框架允许方便地使用套接字。然而,据我所见,没有一个是以半异步模式工作的。此外,大多数套接字库都是一个大型框架的一部分,需要您将数百个文件添加到项目中。
通常,在C或C++中使用套接字需要了解BSD套接字API或Windows套接字(WinSock)API(可能两者都需要才能使您的程序跨平台)、系统调用、线程和进程。这有两个影响:编写的代码需要额外的努力才能跨平台,而且您必须进行大量的学习。直观的C++套接字流可以在几分钟内理解,而其他选项则需要数小时的阅读。
与BSD或WinSock替代方案相比的第二个改进是使用C++类和命名空间。特别是WinSock大量使用宏,这会干扰C++编码并可能导致问题。例如,WinSock有一个宏,它将errno
替换为*errno()
,实际上不允许您使用名为errno
的变量。
SocketLib使用socketlib
作为命名空间:每个类、类型和枚举都位于此命名空间中。然而,还有一个包含网络相关信息的命名空间(networking
);这个命名空间也定义在socketlibb
命名空间中。networking
包含三个枚举及其相应的类型:Protocol
和ProtocolType
、Port
和PortNumber
、Family
和FamilyType
。这些类型在其他函数中用作输入参数或结果。socketlib::prvt
命名空间用于内部。
要求
SocketLib需要GGE/Utils包和pThread库才能工作。GGE/Utils包已包含在项目中,pThread头文件也是如此。但是,您可能需要将pthread32.dll复制到Windows目录。目前,它已在Windows XP和CentOS 5.5上进行测试;然而,它的编写和设计是为了在所有*nix类操作系统上工作。
目前,该系统在Microsoft C++ Compiler 14.0(随Visual Studio 2005提供)和GCC 4.1.2上编译。
HostInfo和AddressInfo
我们的前两个类是HostInfo
和AddressInfo
。HostInfo
解析并包含主机拥有的所有地址信息。它基本上是AddressInfo
的集合,其中每个AddressInfo
保存有关特定地址的网络相关信息。AddressInfo
允许轻松访问IP地址和族(IP v4、IP v6)。然而,可以通过获取原始addrinfo
指针来访问其他信息。
HostInfo
类的Resolve
函数可用于解析域名(或IP地址)。还有一个StartResolve
函数可用于异步检查;每当Resolve
完成时,就会调用ResolveComplete
事件。HostInfo
可以用作布尔值来检查解析是否成功。以下示例说明了该系统的用法。它可以打印服务器的多个IP地址。
#include "SocketLib/HostInfo.h"
#include <iostream>
using namespace socketlib;
using namespace std;
void resolved(HostInfo &info) {
if(!info) { //HostInfo can be converted to bool to check result
cout<<"Cannot resolve host"<<endl;
return;
}
foreach(AddressInfo, ai, info) { //Collection iteration
cout<<endl<<"IP address: "<<ai->IPAddress()<<endl;
}
}
void main() {
HostInfo h;
h.ResolveComplete.Register(&resolved);
h.StartResolve("cmpe.emu.edu.tr");
cin.sync();
cin.ignore(1);
return 0;
}
TCPServer
目前,该系统的TCP部分已完成。TCPServer
是监听和接受传入连接的类。首先,应调用Listen
函数将服务器绑定到特定端口。Accept
过程可以同步或异步工作。异步模式会触发ConnectionReceived
事件。如果需要,此事件可以在不同的线程中触发。CallConnRcvedEvtInNThrd
属性控制此行为。ConnectionReceived
事件使用TCPServer::accept_param
作为参数对象,该对象包含接受的TCPSocketStream
。当客户端之一失去连接时,会触发ConnectionLost
事件。ConnectionLost
事件使用TCPServer::connlost_params
作为参数对象,该对象包含断开连接的TCPSocketStream
。该系统还提供安全的资源分配,即当服务器对象被销毁时,所有连接都将被断开(触发ConnectionLost
事件),接受线程将被终止,端口将被释放,所有资源都将被释放。
以下是TCPServer
方法的列表
Listen( port )
:将服务器绑定到指定的端口;它可以是PortNumber
(可从networking::Port::portname语法获得)、整数端口号或端口表示字符串(如http、ftp等……数字也接受)。StartAccept( )
:开始异步接受新连接。TCPSocketStream &Accept( Timeout )
:接受连接;如果未指定Timeout
,此函数将无限期等待直到收到连接或套接字关闭。TCPServer::Status getStatus( )
:此函数返回服务器的当前状态。它可以是以下之一Idle
:服务器不执行任何操作Listening
:服务器正在监听指定端口,但未接受任何连接Accepting
:服务器正在异步接受连接BlockingAccept
:服务器阻塞在Accept
函数中StopListening( )
:关闭服务器套接字,从而解绑端口并停止异步接受线程(如果正在运行)CloseAll( )
:关闭所有连接int LiveConnections( )
:返回实时连接的总数
以下是一个简单的服务器,它向每个连接的客户端发送“Hello”然后断开连接
#include <iostream>
#include "SocketLib/TCPServer.h"
using namespace std;
using namespace socketlib;
void connect(TCPServer::accept_params params) {
cout<<"Connection received from "<<params.addrinfo.IPAddress()<<endl;
params.socket<<"Hello"<<endl;
params.socket.Close();
}
int main() {
TCPServer server;
server.Listen("444");
server.ConnectionReceived.Register(&connect);
server.StartAccept();
cin.sync();
cin.ignore(1);
return 0;
}
TCPSocketStream / TCPClient
该类有两个不同的名称:TCPSocketStream
和TCPClient
。它的主要目的是在两个套接字之间流式传输数据。它的第二个目标是作为客户端连接到服务器。因此,它也包含连接功能。该类源自标准I/O流。这意味着任何可以插入或从流中提取的对象都可以插入和从该类中提取。然而,套接字不支持查找,因此任何查找或位置请求都将失败。如果您尝试在套接字关闭时发送数据,您将收到SocketException
异常,该异常可能被流系统处理并转换为失败状态。然而,如果读取操作由于连接丢失而失败,您将收到EOF通知。此外,如果对象被销毁,连接将被关闭。
此流的Buffer
类具有两个不同的缓冲区,分别用于传入和传出数据。因此,发送和接收都可以同时进行。然而,Microsoft的流操作头文件为输入和输出缓冲区使用单个互斥体。因此,如果使用Microsoft头文件,同时发送和接收的优势将丢失。
标准插入器是向接收方发送数据的首选方法。此外,还为方便起见,在系统中添加了WriteBinary
函数。对于任何简单对象,您可以使用此函数将其全部数据发送到另一端。数据将在显式刷新请求时发送,或者在缓冲区满时发送。endl
流修改器也会刷新缓冲区,因此它可以用于终止需要发送到接收方的命令。发送请求始终是同步的,但数据传输到操作系统后,它会被排队等待发送;您的应用程序不会等待整个发送操作完成。TCP套接字有一个重要注意事项:发送数据时,数据可能需要被分解成段。段的大小由操作系统或底层硬件控制;因此,无法确定每个数据包都在一个发送请求中发送。
接收数据以半异步模式工作。在此模式下,一个线程始终等待读取数据,另一个线程用于触发Received
事件。第一个线程在建立连接时启动,在连接关闭时停止。第二个线程在收到数据且没有接收请求时启动。第二个线程触发Received
事件并等待其终止。Received
事件的Parameter
对象包含接收缓冲区的大小和一个名为shouldrecall
的引用类型布尔变量。如果在事件处理程序中将此变量设置为true
,并且缓冲区中仍有剩余数据,则会再次触发Received
事件。此方法可用于每次触发Received
事件时仅读取一帧,将剩余数据延迟到第二次调用。在Received
事件中,程序员应使用提取器、get
、getline
、read
或ReadBinary
函数读取数据。提取操作是同步的,但由于缓冲区中有数据(每当触发Received
事件时,缓冲区肯定包含数据)并且可以使用事件参数确定数据的大小,因此此方法可以用作异步机制。一个重要的注意事项是,事件线程与主线程是分离的,可能需要同步线程。
以下是TCPSocketStream
所有方法和变量的列表
bool Connect( host, port )
:解析并连接到给定的主机和端口;此函数是同步的;异步版本是未来的工作。如果它无法解析主机或连接失败,此函数将返回false
;如果发生其他错误,它将抛出SocketException
。bool Connect( addressinfo )
:使用AddressInfo
类中的信息连接到给定主机。要使此系统工作,您必须在HostInfo
类的Resolve
函数中指定端口参数。bool isConnected()
:返回此套接字是否已连接。Close()
:关闭套接字,结束接受线程。此函数可以安全地在接收事件线程中使用;但是,您不能在接收事件线程中销毁调用套接字。int Available()
:读取缓冲区中可用数据的量。Disconnected
事件:每当套接字断开连接时触发。没有特定的参数。
以下是一个连接到服务器并显示任何接收数据的简单客户端
#include <iostream>
#include "SocketLib/TCPSocketStream.h"
using namespace std;
using namespace socketlib;
void received(TCPSocketStream::accept_received_params params,
TCPSocketStream &socket) {
int cnt=params.available;
if(cnt>1024) {
cnt=1024;
}
char data[1024];
socket.read(data, cnt);
cout.write (data, cnt);
params.shouldrecall=true;
//If data in the buffer is larger than 1k
//this event handler will be called again
}
void disconnected() {
cout<<"Disconnected."<<endl;
}
int main() {
TCPSocketStream client;
client.Received.Register(&received);
client.Disconnected.Register(&disconnected);
client.Connect("localhost", "444");
cin.sync();
cin.ignore(1);
return 0;
}
关注点
此库易于使用且无需操心。编写简单的网络应用程序非常容易。由于其固有的多线程性,您无需担心阻塞模式。此外,Receive
事件的shouldrecall
参数有助于处理连接的帧。
使用此库还可以做一件有趣的事情;使用此系统,您可以轻松编写一个将连接到自身并发送数据的应用程序。有效地同时充当客户端和服务器。
历史
- 2011-01-25:首次公开发布。
- 2011-02-01:在CentOS 5.5和GCC 4.1.2上测试后,修改了系统要求,以反映该系统可以在使用GCC的GNU/Linux系统上编译。