4 使用Sockets
4.1 概述
INET Socket API 在标准 OMNeT++ 消息传递接口的基础上为多个通信协议提供了特殊的 C++ 抽象。
套接字最常用于应用程序和路由协议,以获取相应的协议服务。套接字能够与底层协议进行双向通信。它们可以组装和发送服务请求和数据包,也可以接收服务指示和数据包。
应用程序可以简单地调用套接字类的成员函数(如 bind()、connect()、send()、close())来创建和配置套接字,以及发送和接收数据包。应用程序还可以模拟使用多个不同的套接字。
下面几节首先介绍套接字的共享功能,然后详细列出所有 INET 套接字,主要是通过举例说明许多常见用法。
注
为简洁起见,本章中的代码片段有所简化。例如,省略了一些virtual修饰符和override限定符,并简化了一些算法以方便理解。
4.2 Socket接口
虽然套接字总是作为特定协议的 C++ 类来实现,但 INET 也提供了 C++ 套接字接口。这些接口允许编写通用的 C++ 代码,可以同时处理多种不同类型的套接字。
例如,所有套接字都实现了 ISocket 接口,所有网络协议套接字都实现了 INetworkSocket 接口。
4.3 识别Sockets
所有套接字都有一个套接字标识符,该标识符在网络节点内是唯一的。该标识符在套接字创建时自动分配给套接字。在套接字的整个生命周期内,都可以通过 getSocketId() 访问该标识符。
套接字标识符也在 SocketReq 和 SocketInd 数据包标记中传递。这些标记允许应用程序和协议识别Packet、Request和Indication所属的套接字。
4.4 配置Sockets
由于所有套接字都在hoods下进行消息传递,因此在使用前必须对其进行配置。为了将数据包和服务请求发送到底层通信协议的正确网关上,必须对输出网关进行配置:
socket.setOutputGate(gate("socketOut")); // configure socket output gate
socket.setCallback(this); // set callback interface for message processing
相反,底层通信协议的服务指示等传入信息可以在任何应用门上接收。
为了方便应用程序开发,所有套接字都支持存储用户指定的数据对象指针。该指针可通过 setUserData() 和 getUserData() 成员函数访问。
所有套接字的另一项必须配置是设置套接字Callback Interface。下一节将详细介绍Callback Interface。
还可使用其他Sockets特定的配置选项,这些选项将在相应Sockets的章节中讨论。
4.5 Callback Interface
为了便于集中处理信息,所有套接字都提供了一个Callback Interface,应用程序必须实现该接口。Callback Interface通常称为 ICallback,它被定义为其所属套接字的内部类。这些接口通常包含一些通用通知方法和几个套接字专用方法。
例如,最常见的回调方法是处理传入数据包的方法:
class ICallback // usually the inner class of the socket
{
void socketDataArrived(ISocket *socket, Packet *packet);
};
4.6 处理消息
一般来说,套接字可以处理底层协议发送的所有传入信息。接收到的信息必须由其所属的套接字处理。
例如,应用程序可以简单地以任意顺序浏览每个Socket,并决定由哪个Socket来处理接收到的信息,如下所示:
if (socket.belongsToSocket(message)) // match message and socket
socket.processMessage(message); // invoke callback interface
套接字通常会对接收到的信息进行解构,并在必要时更新其状态。套接字还会自动将接收到的数据包和服务指示分派给相应 ICallback 接口中的适当函数,以便进一步处理。
4.7 发送数据
所有套接字都提供一个或多个 send() 函数,使用套接字的当前配置发送数据包。数据包的实际发送方式取决于底层通信协议,但一般来说,套接字的状态会对其产生影响。
例如,套接字配置妥当后,应用程序无需附加任何标签即可开始发送数据包,因为套接字会处理必要的技术细节:
socket.send(packet); // by means of the underlying communication protocol
4.8 接收数据
例如,应用程序可以直接实现套接字的 ICallback 接口,并打印接收到的数据,如下所示:
class App : public cSimpleModule, public ICallback
{
void socketDataArrived(ISocket *socket, Packet *packet);
};
void App::socketDataArrived(ISocket *socket, Packet *packet)
{
EV << packet->peekData() << endl;
}
4.9 关闭Socket
在删除套接字之前,必须先关闭套接字。关闭套接字允许底层通信协议释放已分配的资源。这些资源通常分配在本地网络节点、远程网络节点或网络的其他地方。
例如,面向连接协议的套接字必须关闭,以释放对等方分配的资源:
socket.close(); // release allocated local and remote network resources
4.10 使用多个Socket
如果应用程序需要管理大量Socket,例如在处理多个传入连接的服务器应用程序中,通用 SocketMap 类可能会很有用。该类可以同时管理各种实现 ISocket 接口的套接字。
例如,处理传入数据包或服务指示的步骤如下:
auto socket = socketMap.findSocketFor(message); // lookup socket to process
socket->processMessage(message); // dispatch message to callback interface
为了使 SocketMap 正常运行,必须使用 addSocket() 和 removeSocket() 方法分别向其中添加和删除套接字。
4.11 UDP Socket
UdpSocket 类为发送和接收 UDP 数据报提供了一个易于使用的 C++ 接口。底层 UDP 协议在 Udp 模块中实现。
4.11.1 Callback Interface
处理从 Udp 模块接收到的数据包和指示非常简单。如常规部分所示,传入的信息必须由其所属的套接字处理。
UdpSocket 会解构报文,并使用 UdpSocket::ICallback 接口通知应用程序已收到的数据和错误指示:
class ICallback // inner class of UdpSocket
{
void socketDataArrived(UdpSocket *socket, Packet *packet);
void socketErrorArrived(UdpSocket *socket, Indication *indication);
};
4.11.2 配置Sockets
要在套接字上接收 UDP 数据报,必须将其与地址和端口绑定。地址和端口都是可选的。如果地址未指定,则接收所有目标地址的 UDP 数据报。如果端口为-1,则 Udp 模块会自动选择一个未使用的端口。在同一网络节点内,地址和端口必须是唯一的。
以下是绑定到特定本地地址和端口以接收 UDP 数据报的方法:
socket.bind(Ipv4Address("10.0.0.42"), 42); // local address/port
如果只接收来自特定远程地址/端口的 UDP 数据报,可将套接字连接到所需的远程地址/端口:
socket.connect(Ipv4Address("10.0.0.42"), 42); // remote address/port
还有其他一些套接字选项(如接收广播、管理多播组、设置服务类型)也可以使用 UdpSocket 类进行配置:
socket.setTimeToLive(16); // change default TTL
socket.setBroadcast(true); // receive all broadcasts
socket.joinMulticastGroup(Ipv4Address("224.0.0.9")); // receive multicasts
4.11.3 发送数据
套接字配置完成后,应用程序就可以通过简单的函数调用向远程地址和端口发送数据报:
socket.sendTo(packet42, Ipv4Address("10.0.0.42"), 42); // remote address/port
如果应用程序要发送多个数据报,可以选择连接目的地。
UDP 协议实际上是无连接协议,因此当 Udp 模块收到连接请求时,它只会记住远程地址和端口,并将其作为以后发送的默认目的地。
socket.connect(Ipv4Address("10.0.0.42"), 42); // remote address/port
socket.send(packet1); // send packets via connected socket
// ...
socket.send(packet42);
应用程序可以在同一个套接字上多次调用 connect。
4.11.4 接收数据
例如,应用程序可以直接实现 UdpSocket::ICallback 接口,并打印接收到的数据如下:
class UdpApp : public cSimpleModule, public UdpSocket::ICallback
{
void socketDataArrived(UdpSocket *socket, Packet *packet);
};
void UdpApp::socketDataArrived(UdpSocket *socket, Packet *packet)
{
EV << packet->peekData() << endl;
}
4.12 TCP Socket
TcpSocket 类提供了一个易于使用的 C++ 接口,用于管理 TCP 连接以及发送和接收数据。底层 TCP 协议在 Tcp、TcpLwip 和 TcpNsc 模块中实现。
4.12.1 Callback Interface
从各种 Tcp 模块接收的信息可由其所属的 TcpSocket 处理。TcpSocket 会解构报文,并使用 TcpSocket::ICallback 接口将收到的数据或服务指示通知应用程序:
class ICallback // inner class of TcpSocket
{
void socketDataArrived(TcpSocket* socket, Packet *packet, bool urgent);
void socketAvailable(TcpSocket *socket, TcpAvailableInfo *info);
void socketEstablished(TcpSocket *socket);
// ...
void socketClosed(TcpSocket *socket);
void socketFailure(TcpSocket *socket, int code);
};
4.12.2 配置连接
Tcp 模块支持多种不同的 TCP 拥塞算法,这些算法也可以使用 TcpSocket 进行配置:
socket.setTCPAlgorithmClass("TcpReno");
设置好各个参数后,套接字会立即向底层 Tcp 协议模块发送设备请求。
4.12.3 设置连接
由于 TCP 是一种面向连接的协议,因此在应用程序交换数据之前必须先建立连接。一方面,应用程序通过本地地址和端口监听传入的 TCP 连接:
socket.bind(Ipv4Address("10.0.0.42"), 42); // local address/port
socket.listen(); // start listening for incoming connections
在另一端,应用程序连接到远程地址和端口,建立新的连接:
socket.connect(Ipv4Address("10.0.0.42"), 42); // remote address/port
4.12.4 接受连接
Tcp 模块会自动通知 TcpSocket 有关传入连接的信息。套接字反过来使用回调接口的 ICallback::socketAvailable 方法通知应用程序。最后,传入的 TCP 连接必须被应用程序接受后才能使用:
class TcpServerApp : public cSimpleModule, public TcpSocket::ICallback
{
TcpSocket serverSocket; // server socket listening for connections
SocketMap socketMap; // container for all accepted connections
void socketAvailable(TcpSocket *socket, TcpAvailableInfo *info);
};
void TcpServerApp::socketAvailable(TcpSocket *socket, TcpAvailableInfo *info)
{
auto newSocket = new TcpSocket(info); // create socket using received info
// ...
socketMap.addSocket(newSocket); // store accepted connection
serverSocket.accept(info->getNewSocketId()); // notify Tcp module
}
连接被接受后,Tcp 模块会通知应用程序套接字已建立并准备就绪。
4.12.5 发送数据
连接建立后,应用程序可通过简单的函数调用向远程应用程序发送数据:
socket.send(packet1);
// ...
socket.send(packet42);
数据包数据由本地 Tcp 模块排序,并根据协议逻辑分时段传输。
4.12.6 接收数据
接收数据就像实现 TcpSocket::ICallback 接口的相应方法一样简单。需要注意的是,由于 TCP 协议的特性,数据包数据到达时的块大小可能与发送时不同(但顺序相同)。
例如,应用程序可以直接实现 TcpSocket::ICallback 接口,并打印接收到的数据,如下所示:
class TcpApp : public cSimpleModule, public TcpSocket::ICallback
{
void socketDataArrived(TcpSocket *socket, Packet *packet, bool urgent);
};
void TcpApp::socketDataArrived(TcpSocket *socket, Packet *packet, bool urgent)
{
EV << packet->peekData() << endl;
}