输入和输出(read,recv,recvmsg...和write,writev,writemsg)
首先是在I/O操作上设置超时,三种方法;read和write这两个函数的三个变体:recv和send运行通过第四个参数从进程到内核传递标志:readvhe writev允许指定往其中输入数据或从其中输出数据的缓冲区向量:recvmsg和sendmsg结合了其他I/O函数的所有特性,并具备接受和发送辅助数据的新能力。
套接字设置超时
- 使用信号处理函数alarm,不过这样会涉及到信号处理函数的问题,同时还有可能会引起程序中其他alarm函数的处理
- 使用select函数,在这个函数的最后一个参数中可以设置时间超时
- 使用比较新颖的超时套接字选项SO_RCVTIMEO和SO_SENDTIMEO,属于套接字选项中的内容。并非所有实现都支持这两个套接字选项。
以上这三个技术都适用于输入和输出操作(read、write及其注入recvfrom、sendto之类的变体)。但是TCP内置的connect函数超时默认为75s。select可用来在connect函数上设置超时的先决条件是相应的套接字处于非阻塞模式,而上述的两个套接字选项对connect并不适用,前两种技术适用于任何技术,第三个技术适用于套接字描述符。
一:使用SIGALRM为connect设置超时——1.本技术总能减少connect中断超时期限,但是无法延长内核现有的超时,Berkeley内核connect通常值为75s,如果我们指定一个比75s小的值可以实现,但是如果指定比75s大的值,到75s就返回。
//自定义signal函数
typedef void Sigfunc(int); /* for signal handlers */
Sigfunc *signal1(int signo, Sigfunc *func)
{
struct sigaction act,oact; act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = ; /*标志清零*/
if (signo == SIGALRM)/*被SIGALRM信号中断的系统调用不重新启动*/
{
#ifdef SA_INTERRUPT
act.sa_flags|=SA_INTERRUPT;//SunOS 4.x
#endif // SA_INTERRUPT
}
else
{
#ifdef SA_RESTART
act.sa_flags |= SA_RESTART;//SVR,4.4BSD
#endif // SA_RESTART
}
if (sigaction(signo, &act, &oact) < )
return SIG_ERR;
return oact.sa_handler;
} static void connect_alarm(int signo);
int connect_timeo(int sockfd, const SA *saptr, socklen_t salen, int nsec)
{
Sigfunc *sigfunc;
int n; sigfunc = Signal1(SIGALRM, connect_alarm);
/*如果此前进程已经设置过报警时钟,并且未还未超时,输出错误信息*/
if (alarm(nsec) != )
err_msg("connect_timeo: alarm was already set"); if ((n = connect(sockfd, saptr, salen)) < ) {
/*如果调用被中断,就关闭套接字,防止三路握手继续进行*/
close(sockfd);
if (errno == EINTR)
errno = ETIMEDOUT;
}
/*关闭报警时钟*/
alarm();
/*恢复之前的信号处理函数*/
Signal1(SIGALRM, sigfunc);
return n;
} static void connect_alarm(int signo)
{
return;//just interrupt connect()
}
二.使用SIGALRM为recvfrom设置超时
#include "unp.h"
static void sig_alrm(int);
void
dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
int n;
char sendline[MAXLINE], recvline[MAXLINE + ]; Signal(SIGALRM, sig_alrm);//返回sig_alarm函数指针 while (Fgets(sendline, MAXLINE, fp) != NULL)
{ Sendto(sockfd, sendline, strlen(sendline), , pservaddr, servlen); alarm();//recvfrom之前设置5s超时
if ( (n = recvfrom(sockfd, recvline, MAXLINE, , NULL, NULL)) < )
{
if (errno == EINTR)//调用被信号处理函数中断
fprintf(stderr, "socket timeout\n");
else
err_sys("recvfrom error");
}
else //读到来自服务器的文本
{
alarm();//关掉报警器时钟
recvline[n] = ; /* null terminate */
Fputs(recvline, stdout);
}
}
}
static void
sig_alrm(int signo)//中断被阻塞的recvfrom()
{
return; /* just interrupt the recvfrom() */
}
三.使用select为recvfrom设置超时
//本函数不执行读操作,他只是等待给定的描述符变为可读,因此本函数适用于
//任何类型的套接字,TCP or UDP
int readable_timeo(int fd,int sec)
{
fd_set rset;
struct timeval tv;
FD_ZERO(&rset);
FD_SET(fd,&rset); tv.tv_sec =sec;
tv.tv_usec =;
return (select(fd+,&rset,NULL,NULL,&tv));
}
void dg_cli(FILE*fp,int sockfd,const struct sockaddr*pservaddr,socklen_t servlen)
{
int n;
char sendbuff[MAXLEN];
char recvbuff[MAXLEN+];
while(fgets(sendbuff,MAXLEN,fp)!=NULL)
{
sendto(sockfd,sendbuff,strlen(sendbuff),,pservaddr,servlen);
if(readable_timeo(sockfd,)==)//设置超时等待5秒
{
fprintf(stderr,"socket timeout\n");
}
else//readable_timeo返回正值的时候
{
if((n=recvfrom(sockfd,recvbuff,MAXLEN,,NULL,NULL))<=)
{
printf("recvfrom error\r\n");
return ;
}
recvbuff[n]='\0';
fputs(recvbuff,stdout);
}
}
}
四.使用套接字选项为recvfrom设置超时
SO_RCVTIMEO(读)SO_SENTIMEO(写)两者都不能为connect设置超时,其超时设置将应用于描述符上所有的读操作,一次性设置选项,前面两个方法要求在欲设置时间限制的每个操作发生之前做些工作
void dg_cli(FILE*fp,int sockfd,const struct sockaddr*pservaddr,socklen_t servlen)
{
int n;
char sendbuff[MAXLEN];
char recvbuff[MAXLEN+];
struct timeval tv;
//第四个参数是指向timeval结构的一个指针,填入了期望的超时值
tv.tv_sec=;
tv.tv_usec=;
Setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv)) while(fgets(sendbuff,MAXLEN,fp)!=NULL)
{
sendto(sockfd,sendbuff,strlen(sendbuff),,pservaddr,servlen);
n=recvfrom(sockfd,recvbuff,MAXLEN,,NULL,NULL);
if(n<)
{
if(errno == EWOULDBLOCK)//如果函数超时返回EWOULDBLOCK错误
{
fprintf(stderr,"socket timeout\r\n");
continue;
}
else
fprintf(stderr,"recvfrom error\r\n");
}
recvline[n] = ;
fputs(recvbuff,stdout);
}
}
recv和send
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);
ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);
//返回:成功返回读入或写出的字节数,出错为-1
falgs的值为0或:
flags | 说明 | recv | send |
MSG_DONTROUTE | 绕过路由表查找 | • | |
MSG_DONTWAIT | 仅本操作非阻塞 | • | • |
MSG_OOB | 发送或接收带外数据 | • | • |
MSG_PEEK | 窥看外来消息 | • | |
MSG_WAITALL | 等待所有数据 | • |
这两个函数的前三个参数和read和write的三个参数一样,(都是套接字、缓冲区、缓冲区大小,最后一个参数有一定的讲究,可以简单的设置为0,也可以参考一定的数值设置,其实这个参数就按照默认的0即可,因为其他几种设置没有实际的作用)
Flags参数在设计上存在一个基本问题,它是按值传递的,而不是一个值-结果参数。因此它只能用于从进程向内核传递标志,内核无法向进程传回标志,对于TCP这一点不成问题,以为TCP不需要从内核向进程传递标志。然而随着OSI协议被加到4.3BSD中,却提出了随输入操作向进程返回 MSG_EOR标志的需求。这样最后的决定为保持常用输入函数(recv和recvfrom)的参数不变,而改变recvmsg和sendmsg所用的msghdr结构。这个决定同时意味着如果一个进程需要由内核更新标志,它就必须调用recvmsg,而不是recv或者recvfrom。
readv和writev
分散读和集中写。来自读操作的输入数据被分散到多个应用缓冲区,来自多个应用缓冲区的输出数据被集中提供给单个写操作。
一个4字节的write操作跟一个396个字节的write可能触发Nagle算法,首选办法之一是对这两个缓冲区调用writev
#include <sys/uio.h>
ssize_t readv (int filedes, const struct iovec *iov ,int iovcnt);
ssize_t writev(int filedes, const struct iovec *iov ,int iovcnt);
第二个参数指向某个iovec结构数组的一个指针,可以设置缓冲区的起始地址和大小。另外,这两个操作可以应用于任何描述符,而不是仅限于套接字。另外,writev是一个原子操作,意味着对于一个基于记录的协议(UDP协议)而言,一次调用只产生单个UDP数据报。
struct iovec {
void *iov_base; /* starting address of buffer */
size_t iov_len; /* size of buffer */
};
writev以顺序iov[0],iov[1]至iov[iovcnt-1]从缓冲区中聚集输出数据。writev返回输出的字节总数,通常,它应等于所有缓冲区长度之和。
readv则将读入的数据按上述同样顺序散布到缓冲区中。readv总是先填满一个缓冲区,然后再填写下一个。readv返回读到的总字节数。如果遇到文件结尾,已无数据可读,则返回0。
readv和writev允许单个系统调用读或写操作读入或写出一个或多个缓冲区,成为分散读或集中写,来自读操输入的数据被分散到多个应用缓冲区,来自多个应用缓冲区输入的数据被集中提供给单个写操作,这两个可用于任何描述符
code
#include <sys/uio.h>
#include <stdio.h>
#include <fcntl.h>
int main(int argc,char *argv[])
{
ssize_t size;
char buf1[];
char buf2[];
struct iovec iov[]; fd1=open(argv[],O_RDONLY);
fd2=open(argv[],O_RDONLY);
fd3=open(argv[],O_WRONLY); size=read(fd1,buf1,sizeof(buf1));
printf(“%s size is:%d\n”,argv[],size);
size=read(fd2,buf2,sizeof(buf2));
printf(“%s size is:%d\n”,argv[],size); iov[].iov_base=buf1;
iov[].iov_len=sizeof(buf1);
iov[].iov_base=buf2;
iov[].iov_len=sizeof(buf2); size=writev(fd3,iov,));
printf(“%s size is:%d\n”,argv[],size); close(fd1);
close(fd2);
close(fd3);
return ;
}
recvmsg和sendmsg
#include <sys/socket.h> Ssize_t recvmsg(int sockfd,struct msghdr *msg,int flags);
Ssize_t sendmsg(int sockfd,struct msghdr *msg,int flags);
msghdr
struct msghdr
{
void *msg_name; /* protocol address */
socklen_t msg_namelen; /* sieze of protocol address */
struct iovec *msg_iov; /* scatter/gather array */
int msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* ancillary data ( cmsghdr struct) */
socklen_t msg_conntrollen; /* length of ancillary data */
int msg_flags; /* flags returned by recvmsg() */
}
1.msg_name和msg_namelen用于套接字未连接的时候(主要是未连接的UDP套接字),用来指定接收来源或者发送目的的地址。两个成员分别是套接字地址及其大小,类似recvfrom和sendto的第二和第三个参数。对于已连接套接字,则可直接将两个参数设置为NULL和0。而对于recvmsg,msg_name是一个值-结果参数,会返回发送端的套接字地址。
2.msg_iov和msg_iovlen两个成员用于指定数据缓冲区数组,即iovec结构数组。iovec结构如下:
#include <sys/uio.h>
struct iovec
{
void *iov_base; /* starting address of buffer */
size_t iov_len; /* size of buffer */
}
其中iov_base就是一个缓冲区元素,事实上也是一个数组,而iov_len则是指定该数据的大小。也就是说,缓冲区是一个二维数组,并且每一维长度不是固定的。猜测这样子设置应该是方便传递多个结构类型不同,并且长度也是不固定的数据吧,这样子客户端就可以直接对每个位置的数据进行转换获取就行了。如果只是当存传送一个字符串,那只需要将msg_iovlen设置成1,然后将数据赋给iov[0].iov_base就行了。无论是sendmsg和recvmsg,都需要提前设置好这两项并且分配好内存。
3.msg_control和msg_controllen是用来设置辅助数据的位置和大小的,辅助数据(ancillary data)也叫作控制信息(control infomation)。这两个成员可以用来返回关于数据报文的其他指定信息,不过需要通过setsockopt函数指定要返回的辅助信息。对于sendmsg,这两项需要都设置成0,否则会导致发送数据失败。还未研究过sendmsg的辅助数据能够做什么。
4.只有recvmsg使用msg_flags,recvmsg被调用时,flags参数被复制到msg_lags成员,并且由内核使用其值驱动接受处理过程,内核还是依据recvmsg的结果跟新msg_flags成员的值
5.sendmsg忽略msg_flags成员,因为他直接使用flags参数驱动发送处理过程,这一点意味着在某个sendmsg调用中设置MSG_DONTWAIT标志,那就把flags参数设置为该值,吧msg_flags成员设置该值认为不起作用。
辅助数据
辅助数据就好比控制信息
其中cmsg_level和cmsg_type应该和调用setsockopt函数时传递的level和optname参数是一样的。那么我们怎么获取辅助数据呢,在msg_control辅助数据是通过一个或多个辅助数据对象保存的,辅助数据对象cmsghdr结构如下:
#include <sys/socket.h>
struct cmsghdr
{
socklen_t cmsg_len; /* length in bytes, including this structure */
int cmsg_level; /* originating protocol */
int cmsg_type; /* protocol-specific type */
/* followed by unsigned char cmsg_data[] */
}
辅助数据由一个或多个辅助数据对象(ancillary data object)构成,每个对象以一个定义在头文件<sys/socket.h>中的cmsghdr结构开头
union {
struct cmsghdr cm;
char control[CMSG_SPACE(sizeof(int))];
}control_un;
而辅助数据对象在实际的存储中是如下分布的:
展示了在一个控制缓冲区中出现2个辅助数据对象的一个例子
msg_control指向第一个辅助数据对象,辅助数据的总长度则由msg_controllen指定。每个对象开头都是一个描述该对象的cmsghdr结构。在cmsg_type成员和实际数据之间可以有填充字节,从数据结尾处到下一个辅助数据对象之前也可以有填充字节。
注意,不是所有实现都支持在单个控制缓冲区中存放多个辅助数据对象。
如下图所示,展示了通过一个UNIX域套接口传递描述字或传递凭证时所用的cmsghdr结构的格式。
既然由recvmsg返回的辅助数据可含有任意数目的辅助数据对象,为了对应用程序屏蔽可能出现的填充字节,头文件<sys/socket.h>中定义了以下5个宏,以简化对辅助数据的处理
#include <sys/socket.h>
#include <sys/param.h>
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *mhdrptr);
//返回:指向第一个cmsghdr结构的指针,若无辅助数据则为NULL
struct cmsghdr *CMSG_NXTHDR(struct msghdr *mhdrptr, struct cmsghdr *cmsghdr);
//返回:指向下一个cmsghdr结构的指针,若不再有辅助数据对象则为NULL
unsigned char *CMSG_DATA(struct cmsghdr *cmsgptr);
//返回:指向与cmsghdr结构关联的数据的第一个字节的指针
unsigned char *CMSG_LEN(unsigned int length);
//返回:给定数据量下存放到cmsg_len中的值
unsigned char *CMSG_SPACE(unsigned int length);
//返回:给定数据量下一个辅助数据对象总的大小。
通过上面五个宏我们可以很方便的为msg_control分配内存和遍历辅助对象、获取辅助数据。不过对于分配内存一般需要预先知道要获取的辅助数据结构的大小。
CMSG_LEN和CMSG_SPACE的区别在于,前者不计辅助数据对象中数据部分之后可能的填充字节,因而返回的是用于存放在cmsg_len成员中的值,后者计上结尾处可能的填充字节,因而返回的是用于为辅助对象动态分配空间的大小值。
char contorl[CMSG_SPACE(size_of_struct1) + CMSG_SPACE(size_of_struct2)];
struct msghdr msg;
/* fill in msg structure */
/* call recvmsg() */
struct cmsghdr *cmsgptr;
for ( cmsgptr = CMSG_FIRSTHDR(&msg); cmsgptr != NULL;
cmsgptr = CMSG_NXTHDR(&msg, cmsgptr) ) {
/* 判断是否是自己需要的msg_level和msg_type */
u_char *ptr;
ptr = CMSG_DATA(cmsgptr); /* 获取辅助数据 */
}
注意:
- 对于已连接的套接字,msghdr的msg_name直接设置为NULL,对于recvmsg,该成员会返回对端的套接字地址。
- 对于sendmsg,msghdr的msg_control和msg_controllen需要设置为0,不设置为似乎无法发送成功。
- 处理辅助数据可以直接用5个宏,并且需要根据msg_level和msg_type判断辅助数据的类型再进行相应的转换。unp中讲到的很多cmsg_type可能自己的系统中并没有移植,这点需要注意。比如我使用ubuntu,就没有移植IP_RECVDSTADDR和IP_RECVIF,改用IP_PKTINFO才完成了例子,也是在这里纠结和浪费了很多时间。实际上unp第7章的函数就可以用来判断这些设置项是否存在,也可以在调用setsockopt和判断msg_level、msg_type之前用#if defined语句来判断本系统是否兼容该项,如果不兼容的话会直接跳过接下来的处理(见例子)。
排队的数据量
如果我们想要在不真正读取数据的前提下知道一个套接字上已用多少数据排队等着读取。可用三个技术实现:
- 可以使用非阻塞I/O。
- 如果既想查看数据,又想数据仍然保留在接受队列中以供本进程其他部分稍后读取,那么可以使用MSG_PEEK标志。(需要注意的是:如果使用这个标志来读取套接字上可读数据的大小,在两次调用之间缓冲区可能会增加数据,如果第一次指定使用MSG_PEEK标志,而第二次调用没有指定使用MSG_PEEK标志,那么这两次调用的返回值是一样的,即使在这两次调用之间缓冲区已经增加了数据。)
- 一些实现支持ioctl的FIONREAD命令。该命令的第三个ioctl参数是指向某个整数的一个指针,内核通过该整数返回的值就是套接字接受队列的当前字节数。
套接字和标准I/O
标准I/O: fdopen:从任意描述符创建一个标准I/O流。fileno:获取一个给定标准I/O流对应的描述符
对于标准I/o,+r意味着读写,因为TCP和UDP是全双工的,但是我们一般不这么做,我们为给定的套接字打开两个标准的I/O流,一个读,一个写。
用fdopen打开标准输入和输出,修改服务器回射函数str_echo
void str_echo(int sockfd)
{
char line[MAXLEN];
FILE *fpin=Fdopen(sockfd,"r");//读
FILE *fpout=Fdopen(sockfd,"w");//写
char *x;
while((x=fgets(line,MAXLEN,fpin))!=NULL)
fputs(line,fpout);
}
fdopen创建两个标准I/O流,一个用于输入,一个用于输出,当运行客户,直到输入EOF,才回射所有文本。
实际发生的步骤如下:
- 键入第一行文本,客户端发送到服务器端;
- 服务器fgets到这段文本,并用fputs回射;
- 文本被回射到标准IO函数全缓冲,但不把缓冲区内容写到描述符,因为缓冲区未满;
- 直到输入EOF字符,str_cli调用shutdown,客户端发送一个FIN,服务器收取FIN被fgets读入,返回空指针;
- str_echo函数结束,返回main函数;
- exit调用标准的I/O清理函数,缓冲区中的内容被输出;
- 同时子进程终止,已连接套接字关闭,TCP四分组终止。
这里就有三个概念了:
- 完全缓冲:缓冲区满、fflush、exit,才发生I/O;
- 行缓冲:换行符、fflush、exit,才发生I/O;(标准输入和标准输出是完全缓冲,除非他们指代终端设备此时为行缓冲;所有其他I/O流都是完全缓冲,除非它们指代终端设备此时为行缓冲)
- 不缓冲:每次标准I/O输出函数都发生I/O。(标准错误输入不缓冲)
输入和输出(read,recv,recvmsg...和write,writev,writemsg)的更多相关文章
- 【socket-python应用】控制泓格ET-7044通信模块输入DI输出DO
socket-python应用:控制泓格ET-7044通信模块输入DI输出DO 本节主要内容: 1.socket-python建立TCP通信 2.配合泓格通信模块说明书,查看输入输出寄存器地址,发送指 ...
- 了解一下C++输入和输出的概念
我们经常用到的输入和输出,都是以终端为对象的,即从键盘输入数据,运行结果输出到显示器屏幕上.从操作系统的角度看,每一个与主机相连的输入输出设备都被看作一个文件.除了以终端为对象进行输入和输出外,还经常 ...
- [总结] I/O输入,输出
I/O输入,输出第一:先判断到底是输入还是输出,站在程序的立场第二:判断是传递字节,还是字符,决定管道粗细,字节流是最基本的数据输出管道.字符类型管道专门用来传送文本数据.Java流的四大父类:1.字 ...
- C#语言基础— 输入与输出
C#语言基础— 输入与输出 1.1函数的四要素:名称.输入.输出.加工 1.2主函数:输出语句.输入语句: Static viod Main(string[] stgs)//下划线部分可以自己指定 { ...
- Shell编程基础教程3--Shell输入与输出
3.Shell输入与输出 3.1.echo echo命令可以显示文本行或变量,或者把字符串输出到文件 echo [option] string ...
- 不可或缺 Windows Native (4) - C 语言: 预处理命令,输入,输出
[源码下载] 不可或缺 Windows Native (4) - C 语言: 预处理命令,输入,输出 作者:webabcd 介绍不可或缺 Windows Native 之 C 语言 预处理命令 输入 ...
- 输入和输出的总结(c语言)
c语言中有多种的输入和输出方式,下面就简单总结一下: 一.输入的三种方式 (1)scanf scanf 函数可以在变量中使用,也可以在数组中使用,当然指针上也能用到,是一个很好的输入函数.scanf是 ...
- C++——输入、输出和文件
一.C++输入和输出概述 1.1.流和缓冲区 C++程序把输入和输出看作字节流.输入时,程序从输入流中抽取字节:输出时,程序将字节插入到输出流中.对于面相文本的程序,每个字节代表一个字符,更通俗地说, ...
- C++学习42 输入和输出的概念
我们经常用到的输入和输出,都是以终端为对象的,即从键盘输入数据,运行结果输出到显示器屏幕上.从操作系统的角度看,每一个与主机相连的输入输出设备都被看作一个文件.除了以终端为对象进行输入和输出外,还经常 ...
随机推荐
- 常用DOS命令总结
本文主要参考:http://www.jb51.net/article/12360.htm http://blog.csdn.net/kofterry/article/details/5183110 常 ...
- 【转载】JS Number类型数字位数及IEEE754标准
JS的基础类型Number,遵循 IEEE 754 规范,采用双精度存储(double precision),占用 64 bit.如图 意义 1位用来表示符号位 11位用来表示指数 52位表示尾数 浮 ...
- Vue学习笔记之Vue学习前的准备工作
0x00 起步 1.扎实的HTML/CSS/Javascript基本功,这是前置条件. 2.不要用任何的构建项目工具,只用最简单的<script>,把教程里的例子模仿一遍,理解用法.不推荐 ...
- Python3.x:自动生成IP写入文本
Python3.x:自动生成IP写入文本 ''' 生成ip写入文件 ''' import time time_start = time.time() #参数:number-生成条数:start-开始i ...
- .Net频繁访问数据库的优化探究(一)
知识点:DataTable.Linq.lamda表达式.Cache 场景:查询部门的所有员工和管理员,并分配相应的权限 实现过程一般为:查询部门,遍历部门(查询员工.分配权限.查询管理员.分配权限) ...
- Solidity 官方文档中文版 3_安装Solidity
基于浏览器的Solidity 如果你只是想尝试一个使用Solidity的小合约,你不需要安装任何东西,只要访问 基于浏览器的Solidity http://remix.ethereum.org/. 如 ...
- c语言 找最小值
#include <stdio.h> #define N 10 #define MIN(X,Y) ((X<Y)?(X):(Y)) int f(int arr[],int len,in ...
- Windows__书
1.<<Windows 网络与通信程序设计>> (第2版) 2. 3.
- Java Spring-事务管理
2017-11-12 16:31:59 Spring的事务管理分为两种: 编程式的事务管理:手动编写代码 声明式的事务管理:只需要配置就可以 一.最初的环境搭建 public interface Ac ...
- 在触发器中,当“IsMouseOver”属性=true时,设置当前控件的高亮选中效果
<Style.Triggers> <!--<Trigger Property="IsSelected" Value="Tru ...