TCP4
TCP4协议说明
相比UDP4,TCP4是一种面向连接的通信协议,因此有更好的可靠性。
TCP4的首部格式如下:
各个参数说明如下:
字段 | 长度(bit) | 含义 |
---|---|---|
Source Port | 16 | 源端口,标识哪个应用程序发送。 |
Destination Port | 16 | 目的端口,标识哪个应用程序接收。 |
Sequence Number | 32 | 序号字段。 TCP链接中传输的数据流中每个字节都编上一个序号。 序号字段的值指的是本报文段所发送的数据的第一个字节的序号。 |
Acknowledgment Number | 32 | 确认号。 是期望收到对方的下一个报文段的数据的第1个字节的序号。 即上次已成功接收到的数据字节序号加1。 只有ACK标识为1,此字段有效。 |
Data Offset | 4 | 数据偏移,即首部长度。 指出TCP报文段的数据起始处距离TCP报文段的起始处有多远。 以32比特(4字节)为计算单位。 最多有60字节的首部,若无选项字段,正常为20字节。 |
Reserved | 6 | 保留,必须填0。 |
URG | 1 | 紧急指针有效标识。 它告诉系统此报文段中有紧急数据,应尽快传送(相当于高优先级的数据)。 |
ACK | 1 | 确认序号有效标识。 只有当ACK=1时确认号字段才有效。当ACK=0时,确认号无效。 |
PSH | 1 | 标识接收方应该尽快将这个报文段交给应用层。 接收到PSH = 1的TCP报文段,应尽快的交付接收应用进程,而不再等待整个缓存都填满了后再向上交付。 |
RST | 1 | 重建连接标识。 当RST=1时,表明TCP连接中出现严重错误(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立连接。 |
SYN | 1 | 同步序号标识,用来发起一个连接。 SYN=1表示这是一个连接请求或连接接受请求。 |
FIN | 1 | 发端完成发送任务标识。 用来释放一个连接。 FIN=1表明此报文段的发送端的数据已经发送完毕,并要求释放连接。 |
Window | 16 | 窗口:TCP的流量控制。 窗口起始于确认序号字段指明的值,这个值是接收端期望接收的字节数。 窗口最大为65535字节。 |
Checksum | 16 | 校验字段,包括TCP首部和TCP数据,是一个强制性的字段,一定是由发端计算和存储,并由收端进行验证。 在计算检验和时,要在TCP报文段的前面加上12字节的伪首部。 |
Urgent Pointer | 16 | 紧急指针,只有当URG标志置1时紧急指针才有效。 TCP的紧急方式是发送端向另一端发送紧急数据的一种方式。 紧急指针指出在本报文段中紧急数据共有多少个字节(紧急数据放在本报文段数据的最前面)。 |
Options | 可变 | 选项字段。 TCP协议最初只规定了一种选项,即最长报文段长度(只包含数据字段,不包括TCP首部),又称为MSS。 MSS告诉对方TCP“我的缓存所能接收的报文段的数据字段的最大长度是MSS个字节”。 新的RFC规定有以下几种选型:选项表结束,空操作,最大报文段长度,窗口扩大因子,时间戳。 * 选项表结束。 * 空操作:没有特殊含义,一般用于将TCP选项的总长度填充为4字节的整数倍。 * 最大报文段长度:又称为MSS,只包含数据字段,不包括TCP首部。 * 窗口扩大因子:3字节,其中一个字节表示偏移值S。新的窗口值等于TCP首部中的窗口位数增大到(16+S),相当于把窗口值向左移动S位后获得实际的窗口大小。 * 时间戳:10字节,其中最主要的字段是时间戳值(4字节)和时间戳回送应答字段(4字节)。 |
Padding | 可变 | 填充字段,用来补位,使整个首部长度是4字节的整数倍。 |
data | 可变 | TCP负载。 |
对应UEFI中的代码:
typedef UINT32 TCP_SEQNO;
typedef UINT16 TCP_PORTNO;
//
// TCP header definition
//
typedef struct {
TCP_PORTNO SrcPort;
TCP_PORTNO DstPort;
TCP_SEQNO Seq;
TCP_SEQNO Ack;
UINT8 Res : 4;
UINT8 HeadLen : 4;
UINT8 Flag;
UINT16 Wnd;
UINT16 Checksum;
UINT16 Urg;
} TCP_HEAD;
TCP的连接过程大致如下:
TCP4代码综述
TCP4也是一个通用的网络协议,其实现在NetworkPkg\TcpDxe\TcpDxe.inf,这里首先需要看下它的入口:
EFI_STATUS
EFIAPI
TcpDriverEntryPoint (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
//
// Install the TCP Driver Binding Protocol
//
Status = EfiLibInstallDriverBindingComponentName2 (
ImageHandle,
SystemTable,
&gTcp4DriverBinding,
ImageHandle,
&gTcpComponentName,
&gTcpComponentName2
);
//
// Initialize ISS and random port.
//
Seed = NetRandomInitSeed ();
mTcpGlobalIss = NET_RANDOM (Seed) % mTcpGlobalIss;
mTcp4RandomPort = (UINT16)(TCP_PORT_KNOWN + (NET_RANDOM (Seed) % TCP_PORT_KNOWN));
}
因为TCP4也是一个UEFI Driver Model,所以第一步是安装gTcp4DriverBinding
,其实现:
EFI_DRIVER_BINDING_PROTOCOL gTcp4DriverBinding = {
Tcp4DriverBindingSupported,
Tcp4DriverBindingStart,
Tcp4DriverBindingStop,
0xa,
NULL,
NULL
};
而第二步是初始化一个随机的TCP端口,根据通用网络协议的做法,TCP的端口占两个字节(即16位),只要不是0-1023里面的公认端口都可以,且跟UDP端口的一致也没有关系。
最后还有一个mTcpGlobalIss
,这里的ISS全称是Initial Sending Sequence,它的值本身不是很重要,从名字也知道它的作用就是指定TCP发送的第一个包的序列号,这是因为TCP一次发送的包可能会有很多,所以需要排序。
UDP4在UEFI网络协议栈中的关系图:
Tcp4DriverBindingSupported
TCP4依赖于IP4:
EFI_STATUS
EFIAPI
Tcp4DriverBindingSupported (
IN EFI_DRIVER_BINDING_PROTOCOL *This,
IN EFI_HANDLE ControllerHandle,
IN EFI_DEVICE_PATH_PROTOCOL *RemainingDevicePath OPTIONAL
)
{
//
// Test for the Ip4ServiceBinding Protocol
//
Status = gBS->OpenProtocol (
ControllerHandle,
&gEfiIp4ServiceBindingProtocolGuid,
NULL,
This->DriverBindingHandle,
ControllerHandle,
EFI_OPEN_PROTOCOL_TEST_PROTOCOL
);
return Status;
}
Tcp4DriverBindingStart
Start函数里面只有一个函数TcpCreateService()
,它的作用就是初始化TCP_SERVICE_DATA
。
TCP_SERVICE_DATA
该结构体本身也比较简单:
typedef struct _TCP_SERVICE_DATA {
UINT32 Signature;
EFI_HANDLE ControllerHandle;
EFI_HANDLE DriverBindingHandle;
UINT8 IpVersion;
IP_IO *IpIo;
EFI_SERVICE_BINDING_PROTOCOL ServiceBinding;
LIST_ENTRY SocketList;
} TCP_SERVICE_DATA;
其重点其实就是一个SocketList
,它对应列表成员是SOCKET
。
SOCKET
Socket就是TCP的子项。该结构体如下:
///
/// The socket structure representing a network service access point.
///
struct _TCP_SOCKET {
//
// Socket description information
//
UINT32 Signature; ///< Signature of the socket
EFI_HANDLE SockHandle; ///< The virtual handle of the socket
EFI_HANDLE DriverBinding; ///< Socket's driver binding protocol
EFI_DEVICE_PATH_PROTOCOL *ParentDevicePath;
EFI_DEVICE_PATH_PROTOCOL *DevicePath;
LIST_ENTRY Link;
UINT8 ConfigureState;
SOCK_TYPE Type;
UINT8 State;
UINT16 Flag;
EFI_LOCK Lock; ///< The lock of socket
SOCK_BUFFER SndBuffer; ///< Send buffer of application's data
SOCK_BUFFER RcvBuffer; ///< Receive buffer of received data
EFI_STATUS SockError; ///< The error returned by low layer protocol
BOOLEAN InDestroy;
//
// Fields used to manage the connection request
//
UINT32 BackLog; ///< the limit of connection to this socket
UINT32 ConnCnt; ///< the current count of connections to it
SOCKET *Parent; ///< listening parent that accept the connection
LIST_ENTRY ConnectionList; ///< the connections maintained by this socket
//
// The queue to buffer application's asynchronous token
//
LIST_ENTRY ListenTokenList;
LIST_ENTRY RcvTokenList;
LIST_ENTRY SndTokenList;
LIST_ENTRY ProcessingSndTokenList;
SOCK_COMPLETION_TOKEN *ConnectionToken; ///< app's token to signal if connected
SOCK_COMPLETION_TOKEN *CloseToken; ///< app's token to signal if closed
//
// Interface for low level protocol
//
SOCK_PROTO_HANDLER ProtoHandler; ///< The request handler of protocol
UINT8 ProtoReserved[PROTO_RESERVED_LEN]; ///< Data fields reserved for protocol
UINT8 IpVersion;
NET_PROTOCOL NetProtocol; ///< TCP4 or TCP6 protocol socket used
//
// Callbacks after socket is created and before socket is to be destroyed.
//
SOCK_CREATE_CALLBACK CreateCallback; ///< Callback after created
SOCK_DESTROY_CALLBACK DestroyCallback; ///< Callback before destroyed
VOID *Context; ///< The context of the callback
};
该结构体的创建来自SockCreate()
,其调用流程:
左边的PktRcvdNotify
是IP4中的回调函数,右边就是常用的创建子项的函数。
SOCKET
中的主要成员说明如下:
SockHandle
、NetProtocol
:这两个参数需要一起看,它们的初始化位于SockCreate()
函数中:
Status = gBS->InstallMultipleProtocolInterfaces (
&Sock->SockHandle,
TcpProtocolGuid,
&Sock->NetProtocol,
NULL
);
事实上就是安装了一个Protocol,对应的GUID是TcpProtocolGuid
,它其实就是两个选择,v4和v6,对应到NetProtocol
也就有了两个版本:
if (SockInitData->IpVersion == IP_VERSION_4) {
TcpProtocolGuid = &gEfiTcp4ProtocolGuid;
ProtocolLength = sizeof (EFI_TCP4_PROTOCOL);
} else {
TcpProtocolGuid = &gEfiTcp6ProtocolGuid;
ProtocolLength = sizeof (EFI_TCP6_PROTOCOL);
}
我们需要关注的是gEfiTcp4ProtocolGuid
和EFI_TCP4_PROTOCOL
,后者对应结构体:
struct _EFI_TCP4_PROTOCOL {
EFI_TCP4_GET_MODE_DATA GetModeData;
EFI_TCP4_CONFIGURE Configure;
EFI_TCP4_ROUTES Routes;
EFI_TCP4_CONNECT Connect;
EFI_TCP4_ACCEPT Accept;
EFI_TCP4_TRANSMIT Transmit;
EFI_TCP4_RECEIVE Receive;
EFI_TCP4_CLOSE Close;
EFI_TCP4_CANCEL Cancel;
EFI_TCP4_POLL Poll;
};
就是真正用于收发数据的TCP接口。
DriverBinding
:这个值来自SOCK_INIT_DATA
中的DriverBinding
:
Sock->DriverBinding = SockInitData->DriverBinding;
而后者有来自TCP_SERVICE_DATA
中的DriverBindingHandle
:
mTcpDefaultSockData.DriverBinding = TcpServiceData->DriverBindingHandle;
所以说到底SOCKET
中的DriverBinding
就是TCP_SERVICE_DATA
中的DriverBindingHandle
,最终就是EFI_DRIVER_BINDING_PROTOCOL
中的DriverBindingHandle
。
ParentDevicePath
:它跟上一个参数是有关联的:
Status = gBS->OpenProtocol (
TcpServiceData->ControllerHandle,
&gEfiDevicePathProtocolGuid,
(VOID **)&This->ParentDevicePath,
TcpServiceData->DriverBindingHandle,
This->SockHandle,
EFI_OPEN_PROTOCOL_GET_PROTOCOL
);
实际上就是代表网卡的设备路径,其值以字符串表示大概是这样的:
PciRoot(0x0)/Pci(0x2,0x0)/MAC(525400123456,0x1)
PCI路径可以不用关注,重点在于到MAC为止。
DevicePath
:它是在ParentDevicePath
之上增加了IPv4_DEVICE_PATH
的结果:
Sock->DevicePath = AppendDevicePathNode (Sock->ParentDevicePath, DevicePath);
Status = gBS->InstallProtocolInterface (
&Sock->SockHandle,
&gEfiDevicePathProtocolGuid,
EFI_NATIVE_INTERFACE,
Sock->DevicePath
);
其值以字符串表示大概是这样的:
PciRoot(0x0)/Pci(0x2,0x0)/MAC(525400123456,0x1)/IPv4(0.0.0.0)
Link
:该值与TCP_SERVICE_DATA
中的SocketList
连接。ConfigureState
:表示Socket的配置状态,有以下的值:
///
/// Socket configure state
///
#define SO_UNCONFIGURED 0
#define SO_CONFIGURED_ACTIVE 1
#define SO_CONFIGURED_PASSIVE 2
#define SO_NO_MAPPING 3
Type
:Socket有两种类型,分别是流格式套接字和数据报格式套接字,对应如下代码:
///
/// The socket type.
///
typedef enum {
SockDgram, ///< This socket providing datagram service
SockStream ///< This socket providing stream service
} SOCK_TYPE;
流格式套接字也叫“面向连接的套接字”,它有以下的特征:
- 数据在传输过程中不会消失;
- 数据是按照顺序传输的;
- 数据的发送和接收不是同步的(有的教程也称“不存在数据边界”)。
数据报格式套接字也叫“无连接的套接字”,它有以下的特征:
- 强调快速传输而非传输顺序;
- 传输的数据可能丢失也可能损毁;
- 限制每次传输的数据大小;
- 数据的发送和接收是同步的(有的教程也称“存在数据边界”)。
State
:表示Socket本身的状态,有如下的值:
///
/// Socket state
///
#define SO_CLOSED 0
#define SO_LISTENING 1
#define SO_CONNECTING 2
#define SO_CONNECTED 3
#define SO_DISCONNECTING 4
Flag
:表示TCP头部中的标识,有如下的值:
///
/// Flags in the TCP header
///
#define TCP_FLG_FIN 0x01
#define TCP_FLG_SYN 0x02
#define TCP_FLG_RST 0x04
#define TCP_FLG_PSH 0x08
#define TCP_FLG_ACK 0x10
#define TCP_FLG_URG 0x20
在TCP4协议说明可以找到它们的说明。
SndBuffer
、RcvBuffer
:收发数据使用的缓存。SockError
:Socket的状态。BackLog
:表示Socket连接数上限。ConnCnt
:表示当前的Socket连接数。Parent
:它的类型也是SOCKET
,从这里可以看出来Socket之间也有父子关系。从前面的调用流程可以看到,Socket可以通过SockCreate()
函数创建,而后者又由两个函数调用:
它们对应的入参是不同的,SockCreateChild()
的入参是mTcpDefaultSockData
:
SOCK_INIT_DATA mTcpDefaultSockData = {
SockStream,
SO_CLOSED,
NULL, // Parent
TCP_BACKLOG,
TCP_SND_BUF_SIZE,
TCP_RCV_BUF_SIZE,
IP_VERSION_4,
NULL,
TcpCreateSocketCallback,
TcpDestroySocketCallback,
NULL,
NULL,
0,
TcpDispatcher,
NULL,
};
SockClone()
的实现:
SOCKET *
SockClone (
IN SOCKET *Sock
)
{
SOCKET *ClonedSock;
SOCK_INIT_DATA InitData;
InitData.BackLog = Sock->BackLog;
InitData.Parent = Sock; // 注意这里的Parent
InitData.State = Sock->State;
InitData.ProtoHandler = Sock->ProtoHandler;
InitData.Type = Sock->Type;
InitData.RcvBufferSize = Sock->RcvBuffer.HighWater;
InitData.SndBufferSize = Sock->SndBuffer.HighWater;
InitData.DriverBinding = Sock->DriverBinding;
InitData.IpVersion = Sock->IpVersion;
InitData.Protocol = &(Sock->NetProtocol);
InitData.CreateCallback = Sock->CreateCallback;
InitData.DestroyCallback = Sock->DestroyCallback;
InitData.Context = Sock->Context;
InitData.ProtoData = Sock->ProtoReserved;
InitData.DataSize = sizeof (Sock->ProtoReserved);
ClonedSock = SockCreate (&InitData);
从这里带出了新的父子关系。实际的测试中发现,SockCreateChild()
会在启动中执行,并且Parent
的值都是0,而SockClone()
会在使用TCP时创建Socket,此时的Parent
是一个有效的值。
ConnectionList
:当前Socket维护的连接。ListenTokenList
、RcvTokenList
、SndTokenList
、ProcessingSndTokenList
:处理收发数据的Token列表。ConnectionToken
:Socket连接后调用的Token。CloseToken
:Socket关闭时调用的Token。ProtoHandler
、ProtoReserved
:Socket请求的回调函数以及对应的入参,回调函数就是TcpDispatcher()
,根据入参会执行不同的操作:
EFI_STATUS
TcpDispatcher (
IN SOCKET *Sock,
IN UINT8 Request,
IN VOID *Data OPTIONAL
)
{
switch (Request) {
case SOCK_POLL:
case SOCK_CONSUMED:
case SOCK_SND:
case SOCK_CLOSE:
case SOCK_ABORT:
case SOCK_SNDPUSH:
case SOCK_SNDURG:
case SOCK_CONNECT:
case SOCK_ATTACH:
case SOCK_FLUSH:
case SOCK_DETACH:
case SOCK_CONFIGURE:
case SOCK_MODE:
case SOCK_ROUTE:
default:
}
}
IpVersion
:这里就是IP_VERSION_4
。CreateCallback
、DestroyCallback
、Context
:对应mTcpDefaultSockData
中的函数,以及它们的入参。
EFI_TCP4_PROTOCOL
该Protocol的结构体如下:
///
/// The EFI_TCP4_PROTOCOL defines the EFI TCPv4 Protocol child to be used by
/// any network drivers or applications to send or receive data stream.
/// It can either listen on a specified port as a service or actively connected
/// to remote peer as a client. Each instance has its own independent settings,
/// such as the routing table.
///
struct _EFI_TCP4_PROTOCOL {
EFI_TCP4_GET_MODE_DATA GetModeData;
EFI_TCP4_CONFIGURE Configure;
EFI_TCP4_ROUTES Routes;
EFI_TCP4_CONNECT Connect;
EFI_TCP4_ACCEPT Accept;
EFI_TCP4_TRANSMIT Transmit;
EFI_TCP4_RECEIVE Receive;
EFI_TCP4_CLOSE Close;
EFI_TCP4_CANCEL Cancel;
EFI_TCP4_POLL Poll;
};
对应的实现在NetworkPkg\TcpDxe\TcpDriver.c:
EFI_TCP4_PROTOCOL gTcp4ProtocolTemplate = {
Tcp4GetModeData,
Tcp4Configure,
Tcp4Routes,
Tcp4Connect,
Tcp4Accept,
Tcp4Transmit,
Tcp4Receive,
Tcp4Close,
Tcp4Cancel,
Tcp4Poll
};
相比于其它的网络Protocol,这个稍有不同,它包含了Connect、Accept、Close等TCP常用操作。
后面会介绍这些函数的实现。
Tcp4.Connect
对应的实现是Tcp4Connect
,其实现是Socket的连接:
EFI_STATUS
EFIAPI
Tcp4Connect (
IN EFI_TCP4_PROTOCOL *This,
IN EFI_TCP4_CONNECTION_TOKEN *ConnectionToken
)
{
return SockConnect (Sock, ConnectionToken);
}
其它的Tcp4Accept、Tcp4Transmit、Tcp4Receive、Tcp4Close等也都是Socket的操作。
TCP代码示例
TCP的代码示例可以在beni/BeniPkg/DynamicCommand/TestDynamicCommand/TestTcp.c · jiangwei/edk2-beni - 码云 - 开源中国 (gitee.com)中找到,它实际上来自EmbeddedPkg\Drivers\AndroidFastbootTransportTcpDxe\FastbootTransportTcpDxe.inf,这是一个开源的模块,不过在编译过程中会报错,所以这里进行了移植,对应BeniPkg\Dxe\TransportTcpDxe\TcpTransportDxe.inf,而TestTcp.c模块就是调用了这个模块。
它将开启一个TCP服务端,可以通过TCP客户端(这里使用了Packet Sender,来自Packet Sender - Free utility to for sending / receiving of network packets. TCP, UDP, SSL.)来与它交互。
其运行结果如下:
这里接收数据并打印出来,所以能够在右边Shell下看到左边程序传递过来的数据。
注意这里的Address(192.168.3.128)和Port(1234)是硬编码的,需要根据实际情况修改。