带外数据
带外数据比普通数据具有更高的优先级,应该会立即被发送,不论发送缓冲区中是否有排队等候发送的普通数据,他的传输可以使用一条单独的链路传输也可以映射到普通数据传输的链接中。
如果有多个带外数据,则每个TCP头部都设置URG,他们的紧急指针指向同一位置(数据流中带外数据的下一位置,)只有一个TCP报文段真正携带带外数据。
接收端只有在接收到紧急指针标志时才检查紧急指针,然后根据紧急指针所指的位置确定带外数据的位置,并将它读人一个特殊的缓存中。这个缓存只有1字节,称为带外缓存。如果上层应用程序没有及时将带外数据从带外缓存中读出,则后续的带外数据(如果有的话)将覆盖它。
许多传输层有带外数据的概念,它有时也称为经加速数据。其想法是一个连接的某端发生了重要的事情,而且该端希望迅速通告其对端。这里“迅速”意味着这种通知应该在已排队等待发送的任何“普通”(有时称为“带内”)数据之前发送。也就是说,带外数据被认为具有比普通数据更高的优先级。带外数据并不需要在客户和服务器之间再使用一个连接,而是被映射到已有的连接中。
不幸的是,一旦超越普通概念光临现实世界,我们发现几乎每个传输层都各自有不同的带外数据实现。而UDP作为一个极端的例子,没有实现带外数据。
TCP带外数据
TCP并没有真正的带外数据,不过提供了我们接着要说的紧急模式。假设一个进程已经往一个TCP套接字写出N字节数据,而且TCP把这些数据排队在该套接字的发送缓冲区中,等着发送到对端。如图展示了这样的套接字发送缓冲区,并且标记了从1到N的数据字节。
该进程接着以MSG_OOB标志调用send函数写出一个含有ASCII字符a的单字节带外数据:
send(fd,"a",1,MSG_OOB);
TCP把这个数据放置在该套接字发送缓冲区的下一个可用位置,并把该连接的TCP紧急指针设置成再下一个可用位置。如图展示了此时的套接字发送缓冲区,并且把带外字节标记为:“OOB”。
TCP紧急指针对应一个TCP序列号,它是使用MSG_OOB标志写出的最后一个数据字节(即带外字节)对应的序列号加1。
给定的上图所示的TCP套接字发送缓冲区状态,发送端TCP将为待发送的下一个分节在TCP首部中设置URG标志,并把紧急偏移字段设置为指向带外字节之后的字节,不过该分节可能含也可能不含我们标记的OOB的那个字节。OOB字节是否发送取决于在套接字发送缓冲区中先于它的字节数,TCP准备发送给对端的分节大小以及对端通告的当前窗口。
这是TCP紧急模式的一个重要特点:TCP首部指出发送端已经进入紧急模式(即伴随紧急偏移的URG标志已经设置),但是由紧急指针所指的实际数据字节却不一定随同送出。事实上即使发送端TCP因流量控制而暂停发送数据(接收端的套接字接收缓冲区已满,导致其TCP想发送端TCP通告了一个值为0 的窗口),紧急通知照样不伴随任何数据的发送。这也是应用进程使用TCP紧急模式(即带外数据)的一个原因:即便数据的流动会因为TCP的流量控制而停止,紧急通知却总是无障碍的发送到对端TCP。
紧急指针和紧急偏移在tcp层面是不同的,tcp首部中的16位值称为紧急指针,他必须加上同一个首部的序列号字段才能获得32位的紧急指针,只有在同一个首部中称为URG标志的位已经设置的前提下,tcp才会检查紧急偏移。
如果我们发送多个字节的带外数据,情况又会任何呢?例如:
send(fd,"abc",3,MSG_OOB);
在这个例子中,TCP的紧急指针指向最后那个字节紧后的位置,也就是说最后那个字节(字母c)被认为是带外字节,注意仅仅是一个字节。
至此我们已经讲述了带外数据的发送,下面从接收端的角度查看一下:
1.当收到一个设置了URG标志的分节时,接收端TCP检查紧急指针,确实它是否指向新的带外数据,也就是判断本分节是不是首个到达的引用从发送端到接收端的数据流中特定字节的紧急模式分布。发送端TCP往往发送多个含有URG标志且紧急指针指向同一个数据字节的分节(通常是在已小段时间内)。这些分节中只有第一个到达的会导致通知接收进程有新的带外数据到达。
2.当有新的紧急指针到达时,接收进程被通知到。首先,内核给接收套接字的属主进程发送SIGURG信号,前提是接收进程(或其他进程)曾调用fcntl或ioctl为这个套接字建立了属主,而且该属主进程已为这个信号建立了信号处理函数。其次,如果接收进程阻塞在select调用中以等待这个套接字描述符出现一个异常条件,select调用就返回。
一旦有新的紧急指针到达,不论由紧急指针指向的实际数据字节是否已经到达接收端TCP,这两个潜在通知接收进程的手段就发生动作。
只有一个OOB标记,如果新的OOB字节在旧的OOB字节被读取之前就到达,旧的OOB字节会被丢弃。
3.当由紧急指针指向的实际数据字节到达接收端TCP,该数据字节既可能被拉出带外,也可能被留在带内,即在线留存。SO_OOBINLINE套接字选项默认情况下是禁止的,对于这样的接收端套接字,该数据字节并不放入套接字接收缓冲区,而是被放入该连接的一个独立的单字节带外缓冲区。接收进程从这个单字节缓冲区读入数据的唯一方法是制定MSG_OOB标志调用recv,recvfrom或recvmsg。如果新的OOB字节在旧的OOB字节被读取之前就到达,旧的OOB字节会被丢弃。
然而如果接收进程开启了SO_OOBINLINE套接字选项,那么由TCP紧急指针指向的实际数据字节将被留在通常的套接字接收缓冲区中。这种情况下,接收进程不能指定MSG_OOB标志读入该数据字节。相反,接收进程通过检查该连接的带外标记以获悉何时访问到这个数据字节。
发生一些错误是可能的:
- 如果接收进程请求读入带外数据(通过指定MSG_OOB标志),但是对端尚未发送任何带外数据,读入操作将返回EINVAL。
- 在接收进程已被告知对端发送了一个带外字节(通过SIGURG或select手段)的前提下,如果接收进程读入该字节,但是该字节尚未到达,读入操作将返回EWOULDBLOCK。接收进程此时能做的仅仅是从套接字接收缓冲区读入数据(要是没有存放这些数据的空间,可能还得丢弃他们),以便在该缓冲区中腾出空间,继而允许对端TCP发送出那个带外字节。
- 如果接收进程试图多次读入同一个带外字节,读入操作将返回EINAVL。(后面有说明:select改进)
- 如果接收进程已经开启SO_OOBINLINE套接字选项,后来试图通过指定的MSG_OOB标志读入带外数据,读入操作将返回EINVAL。
使用SIGURG的简单例子
发送和接受带外数据的例子
int main(int argc, char **argv) { int sockfd; if (argc != 3) err_quit("usage: tcpsend01 <host> <port#>"); sockfd = Tcp_connect(argv[1], argv[2]); Write(sockfd, "123", 3); printf("wrote 3 bytes of normal data\n"); sleep(1);//间以停顿的目的是让每个write或send的数据作为单个TCP分节在本端发送并在对端接受 Send(sockfd, "4", 1, MSG_OOB); printf("wrote 1 byte of OOB data\n"); sleep(1); Write(sockfd, "56", 2); printf("wrote 2 bytes of normal data\n"); sleep(1); Send(sockfd, "7", 1, MSG_OOB); printf("wrote 1 byte of OOB data\n"); sleep(1); Write(sockfd, "89", 2); printf("wrote 2 bytes of normal data\n"); sleep(1); exit(0); }
该程序共发送9个字节,每个输出操作之间有一个1S的sleep。停顿的目的是让每个write或send的数据作为单个TCP分节在本端发送并在对端接收。程序运行结果:
wrote 3 bytes of normal date
wrote 1 bytes of OOB data
wrote 2 bytes of normal date
wrote 1 bytes of OOB data
wrote 2 bytes of normal date
下面是接受程序
#include "unp.h" int listenfd, connfd; void sig_urg(int); int main(int argc, char * * argv) { int n; char buff[100]; if(argc == 2) listenfd = Tcp_listen(NULL, argv[1], NULL); else if(argc = 3) listenfd = Tcp_listen(argv[1], argv[2], NULL); else err_quit("usage: tcprecv01 [ <host> ] <port#>"); connfd = Accept(listen, NULL, NULL); Signal(SIGURG, sig_urg); /* 建立SIGURG的信号处理程序 */ Fcntl(connfd, F_SETOWN, getpid()); /* 并用fcntl设置已连接套接口的属主,使用SIGURG前,必须设置socket的宿主进程或进程组(获取异步I/Od的所有权。SIGIO) */ for( ; ; )/* 进程从套接口中读,输出每个由read返回的字符串,当发送者终止连接后,接收者也就终止了 */ { if( (n = Read(connfd, buff, sizeof(buff)-1) ) == 0 ) { printf("recived EOF\n"); exit(0); } buff[n] = 0; /* null terminate */ printf("read %d bytes: %s\n", n, buff); } } void sig_urg(int signo) { int n; char buff[100]; printf("SIGURG received\n"); n = Recv(connfd, buff, sizeof(buff)-1, MSG_OOB); buff[n] = 0; /* null terminate */ printf("read %d OOB byte: %s\n", n, buff); }
直到accept返回之后才建立信号处理函数,这么做会错过一些以小概率出现的带外数据,他们在TCP完成三次握手之后但在accept返回之前到达。然而如果我们在调用accept之前信号处理函数并设置监听套接字的属主(本属性将传承给已连接套接字),那么如果带外数据在accept之前到达,我们的信号处理函数将没有真正的connfd值可用。如果这种情形对于应用程序确实重要,它就应该把connfd初始化为-1,在信号处理函数中检查该值是否为-1,若为真则简单的设置一个标志,供主循环在accept返回之后检查。另一方面,这可能阻塞accept调用周围的信号。
运行结果
read 3 bytes:123
SIGURG received
read 1 OOB byte:4
read 2 bytes:56
SIGURG received
read 1 OOB byte:7
read 2 bytes:89
received EOF
结果与我们预期的一致。发送进程带外数据的每次发送产生递交给接收进程的SIGURG信号,后者接着读入单个带外字节。
使用select的简单例子
select一直指示一个异常条件,直到进程的读入越过带外数据。同一个带外数据不能读入多次,因为首先读入之后,内核就清空这个单字节的缓冲区。再次指定MSG_OOB标志调用recv时,它将返回EINVAL
#include "unp.h" int main(int argc, char **argv) { int listenfd, connfd, n; char buff[100]; fd_set rset, xset; if (argc == 2) listenfd = Tcp_listen(NULL, argv[1], NULL); else if (argc == 3) listenfd = Tcp_listen(argv[1], argv[2], NULL); else err_quit("usage: tcprecv02 [ <host> ] <port#>"); connfd = Accept(listenfd, NULL, NULL); FD_ZERO(&rset); FD_ZERO(&xset); for ( ; ; ) { FD_SET(connfd, &rset); FD_SET(connfd, &xset); Select(connfd + 1, &rset, NULL, &xset, NULL); if (FD_ISSET(connfd, &xset)) { n = Recv(connfd, buff, sizeof(buff)-1, MSG_OOB); buff[n] = 0; /* null terminate */ printf("read %d OOB byte: %s\n", n, buff); } if (FD_ISSET(connfd, &rset)) { if ( (n = Read(connfd, buff, sizeof(buff)-1)) == 0) { printf("received EOF\n"); exit(0); } buff[n] = 0; /* null terminate */ printf("read %d bytes: %s\n", n, buff); } } }
上述代码是错误的,运行结果如下:
read 3 bytes:123
read 1 OOB byte:4
recv error:Invalid argument
解决办法是只在读入普通数据之后select异常条件。它正确的处理了上述情形,以下是正确的代码
#include "unp.h" int main(int argc, char * * argv) { int listenfd, connfd, n, justreadoob = 0; /* 声明了一个叫做justreadoob的变量来指示我们是否刚读过带外数据,这个标志决定是否select异常条件 */ char buff[100]; fd_set rset, xset; if(argc == 2) listenfd = Tcp_listen(NULL, argv[1], NULL); else if(argc == 3) listenfd = Tcp_listen(argv[1], argv[2], NULL); else err_quit("usage: tcprecv03 [ <host> ] <port#>"); connfd = Accept(listenfd, NULL, NULL); FD_ZERO(&rset); FD_ZERO(&xset); for( ; ; ) { FD_SET(connfd, &rset); if(justreadoob == 0) FD_SET(connfd, &xset); Select(connfd+1, &rset, NULL, &xset, NULL); if(FD_ISSET(connfd, &xset)) { n = Recv(connfd, buff, sizeof(buff)-1, MSG_OOB); buff[n] = 0; /* null terminate */ printf("read %d OOB bytes: %s\n", n, buff); justreadoob = 1; /* 当我们设置justreadoob标志时,我们还必须清除这个描述字在异常集合中的那一位 */ FD_CLR(connfd, &xset); } if(FD_ISSET(connfd, &rset)) { if( ( n = Read(connfd, buff, sizeof(buff)-1) ) == 0 ) { printf("received EOF\n"); exit(0); } buff[n] = 0; /* null terminate */ printf("read %d bytes: %s \n", n, buff); justreadoob = 0; } } }
带外标记
讨论的带外数据的接收过程是TCP模块接收带外数据的默认方式。如果我们给TCP连接设置了SO_OOBINLINE选项,则带外数据将和普通数据一样被TCP模块存放在TCP接收缓冲区中。此时应用程序需要像读取普通数据一样来读取带外数据。那么这种情况下如何区分带外数据和普通数据呢?显然,紧急指针可以用来指出带外数据的位置,socket编程接口也提供了系统调用来识别带外数据.
每当收到一个带外数据时,就有一个与之关联的带外标记。这是发送进程发送带外字节时该字节在发送端普通数据流中的位置。在从套接字读入期间,接收进程通过调用sockatmark函数确定是否处于带外标记。
#include <sys/socket.h> int sockatmark(int sockfd);
判断被读到的下一个数据是否有带外数据,如果是返回1,用recv的MSG_OOB接收,不是返回0
本函数时POSIX创造的,如下给出了常见的SIOCATMARK ioctl完成的本函数的一个实现
int sockatmark(int fd) { int flag; if (ioctl(fd, SIOCATMARK, &flag) < 0) return(-1); return(flag != 0); }
不管接收进程在线(SO_OOBINLINE套接字选项)还是带外(MGS_OOB标志)接收带外数据,带外标记都适合。带外标记的常见用法之一是接收进程特殊的对待所有数据,直到标记通过。
我们现在给出一个简单的例子说明带外标记的以下两个特性:
- 带外标记总是指向普通数据最后一个字节紧后的位置。这意味着,如果带外数据在线接收,那么如果下一个待读入的字节时使用MSG_OOB标志发送的,sockatmask就返回真。而如果SO_OOBINLINE套接字选项没有开启,那么,若下一个待读入的字节是跟在带外数据后发送的第一个字节,sockatmark就返回真。
- 读操作总是停在带外标记上。也就是说,如果在套接字接收缓冲区有100个字节,不过在带外标记之前只有5个字节,而进程执行一个请求100个字节的read调用,那么返回的是带外标记之前的5个字节。这种在带外标记上强制停止读操作的做法使得进程能够调用sockatmark确实缓冲区指针是否处于带外标记。
如下是我们的发送程序。它发送3个字节普通数据,1个字节带外数据,再跟1个字节普通数据。每个输出操作之间没有停顿。
例1:
#include "unp.h" int main(int argc, char **argv) { int sockfd; if (argc != 3) err_quit("usage: tcpsend04 <host> <port#>"); sockfd = Tcp_connect(argv[1], argv[2]); Write(sockfd, "123", 3); printf("wrote 3 bytes of normal data\n"); Send(sockfd, "4", 1, MSG_OOB); printf("wrote 1 byte of OOB data\n"); Write(sockfd, "5", 1); printf("wrote 1 byte of normal data\n"); exit(0); }
下面是接收程序。它既不使用SIGURG信号也不使用select。它调用sockatmark来确定何时碰到带外字节
#include "unp.h" int main(int argc, char **argv) { int listenfd, connfd, n, on=1; char buff[100]; if (argc == 2) listenfd = Tcp_listen(NULL, argv[1], NULL); else if (argc == 3) listenfd = Tcp_listen(argv[1], argv[2], NULL); else err_quit("usage: tcprecv04 [ <host> ] <port#>"); Setsockopt(listenfd, SOL_SOCKET, SO_OOBINLINE, &on, sizeof(on)); connfd = Accept(listenfd, NULL, NULL); sleep(5); for ( ; ; ) { if (Sockatmark(connfd)) printf("at OOB mark\n"); if ( (n = Read(connfd, buff, sizeof(buff)-1)) == 0) { printf("received EOF\n"); exit(0); } buff[n] = 0; /* null terminate */ printf("read %d bytes: %s\n", n, buff); } }
读入来自发送进程的所有数据,程序循环调用read,并显示收到的数据。不过在调用read之前,先调用sockatmark检查缓冲区指针是否处于带外标记。
我们运行本程序得到如下输出:
read 3 bytes:123
at OOB mask
read 2bytes:45
recvived EOF
尽管接收进程首次调用read时接收端TCP已经接收了所有数据(因为接收进程调用了sleep),但是首次read调用因遇到带外标记而仅仅返回3个字节即在第四个字节(OOB标记)将会停在这里。下一个读入的字节时带外字节(值为4),因为我们早先告知内核在线放置带外数据。
例2:
我们现在给出另一个简单的例子,用于展示早先提到过的带外数据的另外两个特性。
- 即使因为流量控制而停止发送数据,TCP仍然发送带外数据的通知(即它的紧急指针)。
- 在带外数据到达之前,接收进程可能被通知说发送进程已经发送了带外数据(使用SIGURG信号或通过select)。如果接收进程接着指定MSG_OOB调用recv,而带外数据却尚未到达,recv将返回EWOULDBLOCK错误。
发送端程序
#include "unp.h" int main(int argc, char **argv) { int sockfd, size; char buff[16384]; if (argc != 3) err_quit("usage: tcpsend05 <host> <port#>"); sockfd = Tcp_connect(argv[1], argv[2]); size = 32768; Setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &size, sizeof(size)); Write(sockfd, buff, 16384); printf("wrote 16384 bytes of normal data\n"); sleep(5); Send(sockfd, "a", 1, MSG_OOB); printf("wrote 1 byte of OOB data\n"); Write(sockfd, buff, 1024); printf("wrote 1024 bytes of normal data\n"); exit(0); }
该进程把它的套接字发送缓冲区大小设置为32768,写出16384字节的普通数据,然后睡眠5秒钟。我们稍后将看到接收进程把它的套接字接收缓冲区大小设置为4096,因此发送进程的这些操作确保发送端TCP填满接收端得套接字接收缓冲区。发送进程接着发送单字节的带外数据,后跟1024字节的普通数据,然后终止。
接收端程序
#include "unp.h" int listenfd, connfd; void sig_urg(int); int main(int argc, char **argv) { int size; if (argc == 2) listenfd = Tcp_listen(NULL, argv[1], NULL); else if (argc == 3) listenfd = Tcp_listen(argv[1], argv[2], NULL); else err_quit("usage: tcprecv05 [ <host> ] <port#>"); size = 4096; Setsockopt(listenfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size)); connfd = Accept(listenfd, NULL, NULL); Signal(SIGURG, sig_urg); Fcntl(connfd, F_SETOWN, getpid()); for ( ; ; ) pause(); } void sig_urg(int signo) { int n; char buff[2048]; printf("SIGURG received\n"); n = Recv(connfd, buff, sizeof(buff)-1, MSG_OOB); buff[n] = 0; /* null terminate */ printf("read %d OOB byte\n", n); }
接收进程把监听套接字接收缓冲区大小设置为4096.连接建立之后,这个大小将传承给已连接套接字。接收进程接着accept连接,建立一个SIGURG信号处理函数,并建立套接字的属主。主程序然后再一个无穷循环中调用pause。
信号处理函数调用recv读入带外数据。
先启动接收进程,再启动发送进程
wrote 16384 bytes of normal data
wrote 1 bytes of OOB data
wrote 1024 bytes of normal data
正如所期,所有这些数据适合发送进程套接字发送缓冲区的大小,发送进程手终止。以下是来自接收进程的输出:
SIGURG received
recv error:Resource temporarily unavailable
EAGAIN等于FreeBSD中的EWOULBLOCK
接收进程的输出结果说明发送端TCP向接收端TCP发送了带外通知,由此产生递交给接收进程的SIGURG信号。然而当接收进程指定MSG_OOB标志调用recv时,相应带外字节不能读入因为带外数据还没有到达。
解决办法是让接收进程通知读入已排队的普通数据,在套接字接收缓冲区中腾出空间。这将导致接收端TCP向发送端通告一个非零的窗口,最终允许发送带外字节。
例3:
我们下一个例子展示了一个给定TCP连接只有一个带外标记,如果在接收进程读入某个现有带外数据之前有新的带外数据到达,先前的标记就丢失。
下面是发送程序:
#include "unp.h" int main(int argc, char **argv) { int sockfd; if (argc != 3) err_quit("usage: tcpsend06 <host> <port#>"); sockfd = Tcp_connect(argv[1], argv[2]); Write(sockfd, "123", 3); printf("wrote 3 bytes of normal data\n"); Send(sockfd, "4", 1, MSG_OOB); printf("wrote 1 byte of OOB data\n"); Write(sockfd, "5", 1); printf("wrote 1 byte of normal data\n"); Send(sockfd, "6", 1, MSG_OOB); printf("wrote 1 byte of OOB data\n"); Write(sockfd, "7", 1); printf("wrote 1 byte of normal data\n"); exit(0); }
各个输出调用之间没有停顿,使得所有数据能够迅速的发送到接收端TCP。
接收端,它在接收连接之后睡眠5秒,以允许来自发送端得数据到达接收TCP。以下是接收进程的输出:
read 5 bytes:12345
at OOB mark
read 2bytes:67
received EOF
第二个带外字节(6)的到来覆写了第一个带外字节(4)到来时存放的带外标记。正像我们所说,每个TCP连接最多只有一个带外标记。
总结
- 发送端进入紧急模式这个事实。接收进程得以通知这个事实的手段不外乎SIGURG信号或select调用。本通知在发送进程发送带外字节后由发送端TCP立即发送,即使往接收端的任何数据发送因流量控制而停止了,TCP仍然发送本通知。本通知可能导致接收端进入某种特殊处理模式,以处理接收的任何后继数据。
- 带外字节的位置,也就是它相对于来自发送端的其余数据的发送位置:带外标记。
- 带外字节的实际值。既然TCP是一个不解释应用进程所发送数据的字节流协议,带外字节就可以是任何8位值。
对于TCP的紧急模式,我们可以认为URG标志时通知(信息1),紧急指针是带外标记(信息2),数据字节是其本身(信息3)。
与这个带外数据概念相关的问题有:
- 每个连接只有一个TCP紧急指针;
- 每个连接只有一个带外标记;
- 每个连接只有一个单字节的带外缓冲区(该缓冲区只有在数据非在线读入时才需考虑)。如果带外数据时在线读入的,那么当心的带外数据到达时,先前的带外字节字节并未丢失,不过他们的标记却因此被新的标记取代而丢失了。
带外数据的一个常见的用途体现在rlogin
程序中。当客户中断运行在服务器主机上的程序时,服务器需要告知客户丢弃所有已在服务器排队的输出,因为已经排队等着从服务器发送到客户的输出最多有一个窗口的大小。服务器向客户发送一个特殊字节,告知后者清刷所有这些输出(在客户看来是输入),这个特殊字节就作为带外数据发送。客户收到由带外数据引发的SIGURG信号后,就从套接字中读入直到碰到带外数据发送。客户收到由带外数据引发的SIGURG信号后,就从套接字中读入直到碰到带外标记,并丢弃到标记之前的所有数据。这种情形下即使服务器相继地快速发送多个带外字节,客户也不受影响,因为客户只是读到最后一个标记为止,并丢弃所有读入的数据。
总之,带外数据是否有用取决于应用程序使用它的目的。如果目的是告知对端丢弃直到标记处得普通数据,那么丢失一个中间带外字节及其相应的标记不会有什么不良后果。但是如果不丢失带外字节本身很重要,那么必须在线收到这些数据。另外,作为带外数据发送的数据字节应该区别于普通数据,因为当前新的标记到达时,中间的标记将被覆写,从而事实上把带外字节混杂在普通数据之中。举例来说,telnet在客户和服务器之间普通的数据流中发送telnet自己的命令,手段是把值为255的一个字节作为telnet命令的前缀字节。(值为255的单个字节作为数据发送需要2个相继地值为255的字节。)这么做使得telnet能够区分其命令和普通用户数据,不过要求客户进程和服务器进程处理每个数据字节以寻找命令。