第5节 高级ColdFire TCP/IP客户端设计——Zero-Copy
ColdFire TCP/IP 协议栈支持两种客户端/服务器设计方法。第一种方法是使用类似套接字的BSD接口,被称为mini-IP。使用mini-IP API 的客户端/服务器设计的详细资料参加AN3470的ColdFire TCP/UDP/IP协议栈和RTOS。AN3470还包含了完整的TCP/IP协议栈和如何进行配置。
本节介绍在提高性能的情况下,怎样使用最少的 RAM 来设计一个 TCP/IP客户端。这可以通过zero-copy API直接进入TCP/IP协议栈来实现。
代码的复杂度是在性能和资源优势间的一个权衡。使用mini-IP API 编写客户端或服务器要比使用zero-copy API简单,特别是如果你笔记熟悉BSD套接字编程。
5.1 ColdFire TCP/IP的Zero-Copy
PACKET tcp_pktalloc(int datasize)
通过调用 pk_alloc(datasize + headersize) 来分配一个包缓冲区。在定义宏mini_ip时,设置headersize等于54字节。因为pk_alloc请求一个比bigbufsiz大的帧,所以datasize必须比(bigbufsiz-54)小。PACKET返回的是一个指向netbuf类型结构体的指针。
int tcp_send(M_SOCK so, PACKET pkt)
发送一个tcp_pktalloc分配的分组。该程序将数据复制到*pkt->m_data,字节数复制到 pkt->m_len。正常返回 0,失败则返回错误代码,这些代码类型见msock.h。如果返回0,协议栈获取该分组并在发送完之后将其释放。如果返回错误信息,该程序仍然获取分组并将其返回。
PACKET tcp_recv(M_SOCK so)
返回套接字收到的下一个分组。 pkt->m 指向该分组数据,数据 长度在pkt->m_len 中。程序将数据处理完必须释放此分组。收到分组后返回一个指向netbuf结构体的指针,如果没收到分组并且套接字是非封锁的则返回null。
void tcp_pktfree(PACKET p)
释放p指向的netbuf。
M_SOCK m_socket
分配一个socket结构体,并默认是封锁的。正常返回一个MSOCK结构体,否则返回null。
struct sockaddr_in
{
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
sockaddr_in结构体被广泛地应用于TCP/IP协议栈的API中,sin_family必须被设置为AF_INET。sin_port为16位的端口号。sin_addr为32位的IP地址。sin_zero[]未用。
int m_close(M_SOCK so)
关闭socket中打开的所有连接,并释放该socket。
int m_connect(M_SOCK so, struct sockaddr_in * sin, M_CALLBACK(name))
向服务器开始一个连接进程。m_connect函数试图连接到sockaddr_in结构体里指定的 IP 地址和端口。如果套接字被标定为封锁的,则 m_connect函数只有在 TCPTV_KEEP_INIT(在 mtcp.h 中)定义了一个超时时间才返回,该时间默认为75秒。如果套接字被m_ioctl函数标定为非封锁的,则m_connect函数返回EINPROGRESS,通过调用M_CALLBACK函数,M_CALLBACK参数是用来发送一个完全连接信号。m_connect函数返回的错误代码见msock.h文件。MSOCK是一个指向msocket结构体的指针:
struct msocket
{
struct msocket * next; // queue link
unshort lport; // IP/port describing connection, local port
unshort fport; // far side's port
ip_addr lhost; // local IP address
ip_addr fhost; // far side's IP address
struct tcpcb * tp; // back pointer to tcpcb
struct m_sockbuf sendq; // packets queued for send, including unacked
struct m_sockbuf rcvdq; // packets received but undelivered to app
struct m_sockbuf oosq; // packets received out of sequence
int error; // last error, from BSD list
int state; // bitmask of SS_ values from sockvar.h
int so_options; // bitmask of SO_ options from socket.h
int linger; // linger time if SO_LINGER is set
M_CALLBACK(callback); // socket callback routine
NET ifp; // iface for packets when connected
char t_template[40); // tcp header template, pointed to by tp->template
void * app_data; // for use by socket application
};
5.2 回调(Callback)函数
协议栈的TCP状态机中的hooks在一定条件下调用用户函数。这些hook被称为回调。回调函数提供一种从协议栈异步获取信息的方法,概念类似于中断。
回调函数可以从 TCP 状态机中直接调用,所以要确保回调函数不在睡眠状态且没有长的延时。回调函数应当被视为中断处理例程(ISR),虽然它并不总是在中断里被调用。
程序在调用 m_connect 时提供了回调函数的地址。协议栈将该地址存放在socket结构体中,回调函数的声明如下:
int wget_tcp_callback(int code, M_SOCK so, void * data)
在调用时,回调函数会传递一个 code 说明被调用的原因,一个套接字处理(socket handle)和一个数据指针。将数据指针定为void型是因为它可以指向不同类型的数据。这要取决于所report的代码,并不是所有的代码都提供数据的。回调函数可以在表3所示的情况下被调用。
5.2.1 Packet结构体
当接收到一个分组,就会将void型data指向一个PACKET结构体,并调用回调函数,PACKET结构体属于 netbuf 型。PACKET结构体中的大部分数据都不会被应用程序用到的。两个关键要素是m_data和m_len。
m_data——指向packet中实际的用户数据
m_len——数据长度。
typedef struct netbuf * PACKET;
struct netbuf
{
struct netbuf * next; // 队列链表
char * nb_buff; // raw 缓冲区的开始处
unsigned nb_blen; // raw 缓冲区的大小
char * nb_prot; // 协议/数据的开始处
unsigned nb_plen; // 协议/数据的长度
long nb_tstamp; // packet的时间戳
struct net * net; // the interface (net) it came in on, 0-n
ip_addr fhost; //与packet相关的IP 地址
unsigned short type; // recever(rx) 或网络层(tx)设置IP==0800
unsigned inuse; // use count, for cloning buffer
unsigned flags; // PKF_ defines的 bitmask
#ifdef MINI_TCP // Mini TCP没有mbuf wrappers, 所以:
char * m_data; // 指向nb_buff内的TCP数据
unsigned m_len; // m_data的长度
struct netbuf * m_next; // sockbuf 队列链表
#endif /* MINI_TCP */
struct ip_socopts *soxopts; // socket 选项
};
5.2.2 Callback函数从Packet访问数据的实例
PACKET pkt; // pkt is a pointer to a netbuf structure
// pkt->m_len = Number of Bytes Received
// pkt->m_data = Data Buffer
for(i=0; i<pkt->m_len; i++)
ns_printf(lpio, %c, pkt->m_data[i]);
5.2.3 Callback函数返回值
回调函数返回0或非0。除了M_RXDATA代码外,协议栈每次都忽略callback函数的返回功能。如果callback函数返回0,那么协议栈在callback返回时立即释放packet。
这点是很重要的,因为如果协议栈立即释放packet,tcp_recv或m_recv函数就不能读这个packet了。如果打算在callback内直接处理数据则返回0。如果打算通过tcp_recv或m_recv来唤醒另一个任务处理数据,则返回非0值。
5.2.4 Callback函数实例
//*******************************************************************************
//int wget_tcp_callback(int code, M_SOCK so, void * data)
//
// 该函数从TCP状态机中直接调用
// 当该函数被调用时,code 被赋的值是基于TCP栈所处的状态的
// data 作为void型计算,因为它可指向不同类型数据,取决于code
//
// M_CLOSED – 表示socket已关闭
// so = Socket handle
// data 指向NULL
//
// M_OPENERR – 未用
//
// M_OPENOK – 表示到foreign主机的连接已建立
// so = Socket handle
// data 指向 NULL
//
// M_TXDATA – 表示远程主机已ACK发送的数据
// so = Socket handle
// data指向NULL
//
// M_RXDATA – 表示数据已收到
// so = Socket handle
// data 指向 PACKET 结构体
// 如果返回0,则协议栈释放了该分组
// 如果返回非0,用户必须释放分组!!!!!
//*******************************************************************************
int wget_tcp_callback(int code, M_SOCK so, void * data)
{
PACKET pkt;
int i, k;
int e = 0;
switch(code)
{
case M_OPENERR: // 未用
break;
case M_OPENOK: //表示到foreign主机的连接已建立
break;
case M_CLOSED: // 表示socket已关闭
sclose = 1;
break;
case M_RXDATA: // 表示数据已收到
srx = EMG_HTTP_CLIENT_RX_TIME_SECS;
pkt = (PACKET)data;
// pkt->m_len = 接收到的字节数
// pkt->m_data = 数据缓冲区
// 由于要返回0,所以释放packet
for(i=0; i<pkt->m_len; i++)
{
ns_printf(lpio, %c, pkt->m_data[i));
k = emg_content_length_filter(pkt->m_data[i], &filter_index,&filter_length);
if(k == 3) sclose = 1;
if(k ==2)
{
if(filter_length)
{
filter_length--;
if(filter_length == 0)
sclose = 1;
}
}
}
break;
case M_TXDATA: // 表示远程主机已ACK发送的数据
break;
default:
dtrap(); // 不是一个合法的case
return 0;
}
USE_VOID(data);
return e;
}
5.2.5 利用Zero-Copy API连接到远程服务器
使用zero-copy API 时,可以通过直接访问API提供的分组缓冲区来节省存储空间。通常是先在一个缓冲区创建数据,然后调用m_send函数发送该数据。而m_send函数必须将数据从用户缓冲区复制到分组缓冲区。所以执行该操作需要2倍的RAM空间。
利用zero-copy API,必须分配一个分组缓冲区,然后可以将数据直接嵌入该分组。这样,在传送时就可以减少一个用户缓冲区。接收时RAM优势就取决于该应用程序了。zero-copy的更大的优势在callback函数中。由于收到一个分组就会触发中断,所以该分组很快就可以得到处理,而且缓冲区也会尽可能快地被释放掉。缓冲区释放得越快,需要的 RAM 就越少。这样就间接节省了 RAM,又直接提高了性能。
请求连接到远程服务器的步骤:
1、通过调用m_socket()创建一个socket。
emg_tcp_communication_socket= m_socket( );
2、用IP地址和端口号初始化一个sockaddr_in结构体。
// Init a socket structure with server Port Number
emg_tcp_sin.sin_addr.s_addr = ipaddr;
emg_tcp_sin.sin_port = port;
3、调用m_connect
// Socket is blocking.。The m_connect call blocks until it connects.
e = m_connect(emg_tcp_communication_socket, &emg_tcp_sin, cb);
if(e > 0)
{
e = EMG_HTTP_CLIENT_CONNECT_ERROR_CONNECT;
m_close(emg_tcp_communication_socket);
}
pkt = tcp_pktalloc(llength);
if(pkt)
break;
tk_sleep(EMG_HTTP_CLIENT_TK_SLEEP_SEC);
}
2、将用户数据复制到该分组缓冲区中。
// 将buff指向分组的数据字段
buff = (unsigned char *)pkt->m_data;
// 用url、扩展名和附加的HTTP首部构造GET请求
temp = emgstrcpy((unsigned char *)http_get_request, buff, &buff[llength]);
3、将数据的长度设置在分组缓冲中。
// 将分组的长度字段设置为(int)(temp-buff)
pkt->m_len = (unsigned int)(temp-buff);
4、用tcp_send函数发送数据。
// 发送数据
emg = tcp_send(emg_tcp_communication_socket, pkt); /* 将分组传到TCP层*/
5、如果tcp_send发送失败,则必须释放该分组。
// 如果tcp_send返回错误,则需要释放该分组。
if(emg)
tcp_pktfree(pkt);
return(emg);
本节介绍在提高性能的情况下,怎样使用最少的 RAM 来设计一个 TCP/IP客户端。这可以通过zero-copy API直接进入TCP/IP协议栈来实现。
代码的复杂度是在性能和资源优势间的一个权衡。使用mini-IP API 编写客户端或服务器要比使用zero-copy API简单,特别是如果你笔记熟悉BSD套接字编程。
5.1 ColdFire TCP/IP的Zero-Copy
PACKET tcp_pktalloc(int datasize)
通过调用 pk_alloc(datasize + headersize) 来分配一个包缓冲区。在定义宏mini_ip时,设置headersize等于54字节。因为pk_alloc请求一个比bigbufsiz大的帧,所以datasize必须比(bigbufsiz-54)小。PACKET返回的是一个指向netbuf类型结构体的指针。
int tcp_send(M_SOCK so, PACKET pkt)
发送一个tcp_pktalloc分配的分组。该程序将数据复制到*pkt->m_data,字节数复制到 pkt->m_len。正常返回 0,失败则返回错误代码,这些代码类型见msock.h。如果返回0,协议栈获取该分组并在发送完之后将其释放。如果返回错误信息,该程序仍然获取分组并将其返回。
PACKET tcp_recv(M_SOCK so)
返回套接字收到的下一个分组。 pkt->m 指向该分组数据,数据 长度在pkt->m_len 中。程序将数据处理完必须释放此分组。收到分组后返回一个指向netbuf结构体的指针,如果没收到分组并且套接字是非封锁的则返回null。
void tcp_pktfree(PACKET p)
释放p指向的netbuf。
M_SOCK m_socket
分配一个socket结构体,并默认是封锁的。正常返回一个MSOCK结构体,否则返回null。
struct sockaddr_in
{
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
sockaddr_in结构体被广泛地应用于TCP/IP协议栈的API中,sin_family必须被设置为AF_INET。sin_port为16位的端口号。sin_addr为32位的IP地址。sin_zero[]未用。
int m_close(M_SOCK so)
关闭socket中打开的所有连接,并释放该socket。
int m_connect(M_SOCK so, struct sockaddr_in * sin, M_CALLBACK(name))
向服务器开始一个连接进程。m_connect函数试图连接到sockaddr_in结构体里指定的 IP 地址和端口。如果套接字被标定为封锁的,则 m_connect函数只有在 TCPTV_KEEP_INIT(在 mtcp.h 中)定义了一个超时时间才返回,该时间默认为75秒。如果套接字被m_ioctl函数标定为非封锁的,则m_connect函数返回EINPROGRESS,通过调用M_CALLBACK函数,M_CALLBACK参数是用来发送一个完全连接信号。m_connect函数返回的错误代码见msock.h文件。MSOCK是一个指向msocket结构体的指针:
struct msocket
{
struct msocket * next; // queue link
unshort lport; // IP/port describing connection, local port
unshort fport; // far side's port
ip_addr lhost; // local IP address
ip_addr fhost; // far side's IP address
struct tcpcb * tp; // back pointer to tcpcb
struct m_sockbuf sendq; // packets queued for send, including unacked
struct m_sockbuf rcvdq; // packets received but undelivered to app
struct m_sockbuf oosq; // packets received out of sequence
int error; // last error, from BSD list
int state; // bitmask of SS_ values from sockvar.h
int so_options; // bitmask of SO_ options from socket.h
int linger; // linger time if SO_LINGER is set
M_CALLBACK(callback); // socket callback routine
NET ifp; // iface for packets when connected
char t_template[40); // tcp header template, pointed to by tp->template
void * app_data; // for use by socket application
};
5.2 回调(Callback)函数
协议栈的TCP状态机中的hooks在一定条件下调用用户函数。这些hook被称为回调。回调函数提供一种从协议栈异步获取信息的方法,概念类似于中断。
回调函数可以从 TCP 状态机中直接调用,所以要确保回调函数不在睡眠状态且没有长的延时。回调函数应当被视为中断处理例程(ISR),虽然它并不总是在中断里被调用。
程序在调用 m_connect 时提供了回调函数的地址。协议栈将该地址存放在socket结构体中,回调函数的声明如下:
int wget_tcp_callback(int code, M_SOCK so, void * data)
在调用时,回调函数会传递一个 code 说明被调用的原因,一个套接字处理(socket handle)和一个数据指针。将数据指针定为void型是因为它可以指向不同类型的数据。这要取决于所report的代码,并不是所有的代码都提供数据的。回调函数可以在表3所示的情况下被调用。
表3 回调函数代码


5.2.1 Packet结构体
当接收到一个分组,就会将void型data指向一个PACKET结构体,并调用回调函数,PACKET结构体属于 netbuf 型。PACKET结构体中的大部分数据都不会被应用程序用到的。两个关键要素是m_data和m_len。
m_data——指向packet中实际的用户数据
m_len——数据长度。
typedef struct netbuf * PACKET;
struct netbuf
{
struct netbuf * next; // 队列链表
char * nb_buff; // raw 缓冲区的开始处
unsigned nb_blen; // raw 缓冲区的大小
char * nb_prot; // 协议/数据的开始处
unsigned nb_plen; // 协议/数据的长度
long nb_tstamp; // packet的时间戳
struct net * net; // the interface (net) it came in on, 0-n
ip_addr fhost; //与packet相关的IP 地址
unsigned short type; // recever(rx) 或网络层(tx)设置IP==0800
unsigned inuse; // use count, for cloning buffer
unsigned flags; // PKF_ defines的 bitmask
#ifdef MINI_TCP // Mini TCP没有mbuf wrappers, 所以:
char * m_data; // 指向nb_buff内的TCP数据
unsigned m_len; // m_data的长度
struct netbuf * m_next; // sockbuf 队列链表
#endif /* MINI_TCP */
struct ip_socopts *soxopts; // socket 选项
};
5.2.2 Callback函数从Packet访问数据的实例
PACKET pkt; // pkt is a pointer to a netbuf structure
// pkt->m_len = Number of Bytes Received
// pkt->m_data = Data Buffer
for(i=0; i<pkt->m_len; i++)
ns_printf(lpio, %c, pkt->m_data[i]);
5.2.3 Callback函数返回值
回调函数返回0或非0。除了M_RXDATA代码外,协议栈每次都忽略callback函数的返回功能。如果callback函数返回0,那么协议栈在callback返回时立即释放packet。
这点是很重要的,因为如果协议栈立即释放packet,tcp_recv或m_recv函数就不能读这个packet了。如果打算在callback内直接处理数据则返回0。如果打算通过tcp_recv或m_recv来唤醒另一个任务处理数据,则返回非0值。
5.2.4 Callback函数实例
//*******************************************************************************
//int wget_tcp_callback(int code, M_SOCK so, void * data)
//
// 该函数从TCP状态机中直接调用
// 当该函数被调用时,code 被赋的值是基于TCP栈所处的状态的
// data 作为void型计算,因为它可指向不同类型数据,取决于code
//
// M_CLOSED – 表示socket已关闭
// so = Socket handle
// data 指向NULL
//
// M_OPENERR – 未用
//
// M_OPENOK – 表示到foreign主机的连接已建立
// so = Socket handle
// data 指向 NULL
//
// M_TXDATA – 表示远程主机已ACK发送的数据
// so = Socket handle
// data指向NULL
//
// M_RXDATA – 表示数据已收到
// so = Socket handle
// data 指向 PACKET 结构体
// 如果返回0,则协议栈释放了该分组
// 如果返回非0,用户必须释放分组!!!!!
//*******************************************************************************
int wget_tcp_callback(int code, M_SOCK so, void * data)
{
PACKET pkt;
int i, k;
int e = 0;
switch(code)
{
case M_OPENERR: // 未用
break;
case M_OPENOK: //表示到foreign主机的连接已建立
break;
case M_CLOSED: // 表示socket已关闭
sclose = 1;
break;
case M_RXDATA: // 表示数据已收到
srx = EMG_HTTP_CLIENT_RX_TIME_SECS;
pkt = (PACKET)data;
// pkt->m_len = 接收到的字节数
// pkt->m_data = 数据缓冲区
// 由于要返回0,所以释放packet
for(i=0; i<pkt->m_len; i++)
{
ns_printf(lpio, %c, pkt->m_data[i));
k = emg_content_length_filter(pkt->m_data[i], &filter_index,&filter_length);
if(k == 3) sclose = 1;
if(k ==2)
{
if(filter_length)
{
filter_length--;
if(filter_length == 0)
sclose = 1;
}
}
}
break;
case M_TXDATA: // 表示远程主机已ACK发送的数据
break;
default:
dtrap(); // 不是一个合法的case
return 0;
}
USE_VOID(data);
return e;
}
5.2.5 利用Zero-Copy API连接到远程服务器
使用zero-copy API 时,可以通过直接访问API提供的分组缓冲区来节省存储空间。通常是先在一个缓冲区创建数据,然后调用m_send函数发送该数据。而m_send函数必须将数据从用户缓冲区复制到分组缓冲区。所以执行该操作需要2倍的RAM空间。
利用zero-copy API,必须分配一个分组缓冲区,然后可以将数据直接嵌入该分组。这样,在传送时就可以减少一个用户缓冲区。接收时RAM优势就取决于该应用程序了。zero-copy的更大的优势在callback函数中。由于收到一个分组就会触发中断,所以该分组很快就可以得到处理,而且缓冲区也会尽可能快地被释放掉。缓冲区释放得越快,需要的 RAM 就越少。这样就间接节省了 RAM,又直接提高了性能。
请求连接到远程服务器的步骤:
1、通过调用m_socket()创建一个socket。
emg_tcp_communication_socket= m_socket( );
2、用IP地址和端口号初始化一个sockaddr_in结构体。
// Init a socket structure with server Port Number
emg_tcp_sin.sin_addr.s_addr = ipaddr;
emg_tcp_sin.sin_port = port;
3、调用m_connect
// Socket is blocking.。The m_connect call blocks until it connects.
e = m_connect(emg_tcp_communication_socket, &emg_tcp_sin, cb);
if(e > 0)
{
e = EMG_HTTP_CLIENT_CONNECT_ERROR_CONNECT;
m_close(emg_tcp_communication_socket);
}
5.2.6 用Zero-Copy API发送数据
用Zero-Copy API发送数据的步骤如下:
1、利用tcp_pktalloc函数为数据分配一个足够大的分组缓冲区。
for(i=0; i<EMG_HTTP_CLIENT_PACKET_WAIT_SEC; i++)
{用Zero-Copy API发送数据的步骤如下:
1、利用tcp_pktalloc函数为数据分配一个足够大的分组缓冲区。
for(i=0; i<EMG_HTTP_CLIENT_PACKET_WAIT_SEC; i++)
pkt = tcp_pktalloc(llength);
if(pkt)
break;
tk_sleep(EMG_HTTP_CLIENT_TK_SLEEP_SEC);
}
2、将用户数据复制到该分组缓冲区中。
// 将buff指向分组的数据字段
buff = (unsigned char *)pkt->m_data;
// 用url、扩展名和附加的HTTP首部构造GET请求
temp = emgstrcpy((unsigned char *)http_get_request, buff, &buff[llength]);
3、将数据的长度设置在分组缓冲中。
// 将分组的长度字段设置为(int)(temp-buff)
pkt->m_len = (unsigned int)(temp-buff);
4、用tcp_send函数发送数据。
// 发送数据
emg = tcp_send(emg_tcp_communication_socket, pkt); /* 将分组传到TCP层*/
5、如果tcp_send发送失败,则必须释放该分组。
// 如果tcp_send返回错误,则需要释放该分组。
if(emg)
tcp_pktfree(pkt);
return(emg);


