linux下socket编程-TCP
網(wǎng)絡字節(jié)序
發(fā)送主機通常將發(fā)送緩沖區(qū)中的數(shù)據(jù)按內(nèi)存地址從低到高的順序發(fā)出,接收主機把從網(wǎng)絡上接到的字節(jié)依次保存在接收緩沖區(qū)中,也是按內(nèi)存地址從低到高的順序保存,因此,網(wǎng)絡數(shù)據(jù)流的地址應這樣規(guī)定:先發(fā)出的數(shù)據(jù)是低地址,后發(fā)出的數(shù)據(jù)是高地址。
為使網(wǎng)絡程序具有可移植性,使同樣的C代碼在大端和小端計算機上編譯后都能正常運行,可以調(diào)用以下庫函數(shù)做網(wǎng)絡字節(jié)序和主機字節(jié)序的轉(zhuǎn)換。
#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);例如將IP地址轉(zhuǎn)換后準備發(fā)送。如果主機是小端字節(jié)序,這些函數(shù)將參數(shù)做相應的大小端轉(zhuǎn)換然后返回,如果主機是大端字節(jié)序,這些函數(shù)不做轉(zhuǎn)換,將參數(shù)原封不動地返回?!?/p>
Socket地址的數(shù)據(jù)類型
具體細節(jié):
sockaddr的缺陷:sa_data把目標地址和端口信息混在一起了。
sockaddr_in結(jié)構(gòu)體解決了sockaddr的缺陷,把port和addr 分開儲存在兩個變量中。
對于struct in_addr還有另一種形式的實現(xiàn):
struct in_addr {union{struct{unsigned char s_b1,s_b2,s_b3,s_b4;} S_un_b;struct{unsigned short s_w1,s_w2;} S_un_w;unsigned long S_addr;//4字節(jié),32位,按照網(wǎng)絡字節(jié)順序存儲IP地址} S_un; };只要取得某種sockaddr結(jié)構(gòu)體的首地址,不需要知道具體是哪種類型的sockaddr結(jié)構(gòu)體,就可以根據(jù)地址類型字段確定結(jié)構(gòu)體中的內(nèi)容。因此,socket API可以接受各種類型的sockaddr結(jié)構(gòu)體指針做參數(shù),例如bind、accept、connect等函數(shù),這些函數(shù)的參數(shù)應該設計成void *類型以便接受各種類型的指針,但是sock API的實現(xiàn)早于ANSI C標準化,那時還沒有void *類型,因此這些函數(shù)的參數(shù)都用struct sockaddr *類型表示,在傳遞參數(shù)之前要強制類型轉(zhuǎn)換一下,例如:
struct sockaddr_in servaddr; /* initialize servaddr */ bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));sockaddr_in中的成員struct in_addr sin_addr表示32位的IP地址。但是我們通常用點分十進制的字符串表示IP地址,以下函數(shù)可以在字符串表示和in_addr表示之間轉(zhuǎn)換。
字符串轉(zhuǎn)in_addr的函數(shù):
#include <arpa/inet.h>int inet_aton(const char *strptr, struct in_addr *addrptr); in_addr_t inet_addr(const char *strptr); int inet_pton(int family, const char *strptr, void *addrptr);in_addr轉(zhuǎn)字符串的函數(shù):
char *inet_ntoa(struct in_addr inaddr); const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);其中inet_pton和inet_ntop不僅可以轉(zhuǎn)換IPv4的in_addr,還可以轉(zhuǎn)換IPv6的in6_addr,因此函數(shù)接口是void *addrptr。
TCP協(xié)議通信流程
如果客戶端沒有更多的請求了,就調(diào)用close()關(guān)閉連接,就像寫端關(guān)閉的管道一樣,服務器的read()返回0,這樣服務器就知道客戶端關(guān)閉了連接,也調(diào)用close()關(guān)閉連接。注意,任何一方調(diào)用close()后,連接的兩個傳輸方向都關(guān)閉,不能再發(fā)送數(shù)據(jù)了。如果一方調(diào)用shutdown()則連接處于半關(guān)閉狀態(tài),仍可接收對方發(fā)來的數(shù)據(jù)。
提示:read()返回0就表明收到了FIN段。
最簡單的TCP網(wǎng)絡程序
/*server.c*/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <ctype.h> #include <arpa/inet.h>#define MAXLINE 80 #define SERV_PORT 8000int main(void) {//IP地址+端口號就是一個sokcet,唯一標識網(wǎng)絡通信的一個進程struct sockaddr_in servaddr, cliaddr;socklen_t cliaddr_len;int listenfd, connfd;char buf[MAXLINE];//ipv4的地址長度char str[INET_ADDRSTRLEN];int i, n;listenfd = socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(SERV_PORT);bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));listen(listenfd, 20);printf("Accepting connections ...\n");while(1){cliaddr_len = sizeof(cliaddr);connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);n = read(connfd, buf, MAXLINE);printf("received from %s at port %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));for(i = 0; i < n; ++i){buf[i] = toupper(buf[i]);}//forwrite(connfd, buf, n);close(connfd);}//while}服務器的網(wǎng)絡地址為INADDR_ANY,這個宏表示本地的任意IP地址,因為服務器可能有多個網(wǎng)卡,每個網(wǎng)卡也可能綁定多個IP地址,這樣設置可以在所有的IP地址上監(jiān)聽,直到與某個客戶端建立了連接時才確定下來到底用哪個IP地址,端口號為SERV_PORT,我們定義為8000。
在accept函數(shù)中,cliaddr是一個傳出參數(shù),accept()返回時傳出客戶端的地址和端口號。addrlen參數(shù)是一個傳入傳出參數(shù)(value-result argument),傳入的是調(diào)用者提供的緩沖區(qū)cliaddr的長度以避免緩沖區(qū)溢出問題,傳出的是客戶端地址結(jié)構(gòu)體的實際長度(有可能沒有占滿調(diào)用者提供的緩沖區(qū))。如果給cliaddr參數(shù)傳NULL,表示不關(guān)心客戶端的地址。
/* client.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>#define MAXLINE 80 #define SERV_PORT 8000int main(int argc, char *argv[]) {struct sockaddr_in servaddr;char buf[MAXLINE];int sockfd, n;char *str;if(argc != 2){fputs("usage: ./client message\n", stderr);exit(1);}str = argv[1];sockfd = socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);servaddr.sin_port = htons(SERV_PORT);connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));write(sockfd, str, strlen(str));n = read(sockfd, buf, MAXLINE);printf("Response from server:\n");write(STDOUT_FILENO, buf, n);close(sockfd);return 0; }先編譯運行服務器:
$ ./serverAccepting connections ...然后在另一個終端里用netstat命令查看:
$ netstat -apn|grep 8000tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN 8148/server可以看到server程序監(jiān)聽8000端口,IP地址還沒確定下來。
現(xiàn)在編譯運行客戶端:
$ ./client abcd Response from server: ABCD回到server所在的終端,看看server的輸出:
$ ./serverAccepting connections ...received from 127.0.0.1 at PORT 59757再做一個小實驗,在客戶端的connect()代碼之后插一個while(1);死循環(huán),使客戶端和服務器都處于連接中的狀態(tài),用netstat命令查看:
$ ./server & [1] 8343 $ Accepting connections ... ./client abcd & [2] 8344 $ netstat -apn|grep 8000 tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN 8343/server tcp 0 0 127.0.0.1:44406 127.0.0.1:8000 ESTABLISHED8344/client tcp 0 0 127.0.0.1:8000 127.0.0.1:44406 ESTABLISHED8343/server應用程序中的一個socket文件描述符對應一個socket pair,也就是源地址:源端口號和目的地址:目的端口號,也對應一個TCP連接。
錯誤處理與讀寫控制
系統(tǒng)調(diào)用不能保證每次都成功,必須進行出錯處理,這樣一方面可以保證程序邏輯正常,另一方面可以迅速得到故障信息。
為使錯誤處理的代碼不影響主程序的可讀性,我們把與socket相關(guān)的一些系統(tǒng)函數(shù)加上錯誤處理代碼包裝成新的函數(shù),做成一個模塊wrap.c:
#include <stdlib.h> #include <errno.h> #include <sys/socket.h>void perr_exit(const char *s) {perror(s);exit(1); }int wrap_accept(int fd, struct sockaddr *sa, socklen_t *salenptr) {int n;again:if((n = accept(fd, sa, salenptr)) < 0){if((errno == ECONNABORTED) || (errno == EINTR))goto again;else perr_exit("accept error");}return n; }void wrap_bind(int fd, const struct sockaddr *sa, socklen_t salen) {if(bind(fd, sa, salen) < 0)perr_exit("bind error"); }void wrap_connect(int fd, const struct sockaddr *sa, socklen_t salen) {if(connect(fd, sa, salen) < 0)perr_exit("connect error"); }void wrap_listen(int fd, int backlog) {if(listen(fd, backlog) < 0)perr_exit("listen error"); }int wrap_socket(int family, int type, int protocol) {int n;if((n = socket(family, type, protocol)) < 0)perr_exit("socket error");return n; }ssize_t wrap_read(int fd, void *ptr, size_t nbytes) {ssize_t n; again:if((n = read(fd, ptr, nbytes)) == -1){if(errno == EINTR)goto again;else return -1;}return n; }ssize_t wrap_write(int fd, const void *ptr, size_t nbytes) {ssize_t n;again:if((n = write(fd, ptr, nbytes)) == -1){if(errno == EINTR)goto again;elsereturn -1;}return n; }void wrap_close(int fd) {if(close(fd) == -1)perr_exit("close error"); }慢系統(tǒng)調(diào)用accept、read和write被信號中斷時應該重試。connect雖然也會阻塞,但是被信號中斷時不能立刻重試。對于accept,如果errno是ECONNABORTED,也應該重試。
TCP協(xié)議是面向流的,read和write調(diào)用的返回值往往小于參數(shù)指定的字節(jié)數(shù)。對于read調(diào)用,如果接收緩沖區(qū)中有20字節(jié),請求讀100個字節(jié),就會返回20。對于write調(diào)用,如果請求寫100個字節(jié),而發(fā)送緩沖區(qū)中只有20個字節(jié)的空閑位置,那么write會阻塞,直到把100個字節(jié)全部交給發(fā)送緩沖區(qū)才返回,但如果socket文件描述符有O_NONBLOCK標志,則write不阻塞,直接返回20。為避免這些情況干擾主程序的邏輯,確保讀寫我們所請求的字節(jié)數(shù),我們實現(xiàn)了兩個包裝函數(shù)readn和writen,也放在wrap.c中:
ssize_t wrap_readn(int fd, void *vptr, size_t n) {size_t nleft;ssize_t nread;char *ptr;ptr = vptr;nleft = n;while(nleft > 0){if((nread = read(fd, ptr, nleft)) < 0){if(errno == EINTR)nread = 0;elsereturn -1;}else if(nread == 0){break;}nleft -= nread;ptr += nread;}//whilereturn n - nleft; }如果wrap_readn函數(shù)返回了負數(shù),那么這個負數(shù)的絕對值就表示多讀取了多少的字節(jié)數(shù)。
ssize_t wrap_writen(int fd, const void *vptr, size_t n) {size_t nleft;ssize_t nwritten;const char *ptr;ptr = vptr;nleft = n;while(nleft > 0){if((nwritten = write(fd, ptr, nleft)) <= 0){if(nwritten < 0 && errno == EINTR)nwritten = 0;elsereturn -1;}nleft -= nwritten;ptr += nwritten;}return n; }如果應用層協(xié)議的各字段長度固定,用readn來讀是非常方便的。例如設計一種客戶端上傳文件的協(xié)議,規(guī)定前12字節(jié)表示文件名,超過12字節(jié)的文件名截斷,不足12字節(jié)的文件名用'\0'補齊,從第13字節(jié)開始是文件內(nèi)容,上傳完所有文件內(nèi)容后關(guān)閉連接,服務器可以先調(diào)用readn讀12個字節(jié),根據(jù)文件名創(chuàng)建文件,然后在一個循環(huán)中調(diào)用read讀文件內(nèi)容并存盤,循環(huán)結(jié)束的條件是read返回0。
字段長度固定的協(xié)議往往不夠靈活,難以適應新的變化。如果新版本的協(xié)議要添加新的字段,比如規(guī)定前12字節(jié)是文件名,從13到16字節(jié)是文件類型說明,從第17字節(jié)開始才是文件內(nèi)容,同樣會造成和老版本的程序無法兼容的問題。
現(xiàn)在看一下TFTP協(xié)議是如何避免上述問題的:TFTPTFTP協(xié)議的各字段是可變長的,以'\0'為分隔符,文件名可以任意長,這樣,以后添加新的選項仍然可以和老版本的程序兼容(老版本的程序只要忽略不認識的選項就行了)。因此,常見的應用層協(xié)議都是帶有可變長字段的,字段之間的分隔符用換行的比用'\0'的更常見,例如本節(jié)后面要介紹的HTTP協(xié)議。可變長字段的協(xié)議用readn來讀就很不方便了,為此我們實現(xiàn)一個類似于fgets的readline函數(shù),也放在wrap.c中:
//每次調(diào)用one_char_read返回一個字節(jié)數(shù)據(jù) //字節(jié)數(shù)據(jù)是暫存在靜態(tài)的數(shù)組中的 static ssize_t one_char_read(int fd, char *ptr) {static int read_cnt;static char *read_ptr; //靜態(tài)緩沖區(qū)的遍歷指針static char read_buf[100]; //靜態(tài)數(shù)據(jù)緩沖區(qū)if(read_cnt <= 0){again:if((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0){if(errno = EINTR)goto againreturn -1;}else if(read_cnt == 0)return 0;read_ptr = read_buf;}//ifread_cnt--;*ptr = *read_ptr++;return 1; }當把靜態(tài)緩沖區(qū)的字節(jié)全部返回后,read_cnt=0,下次在調(diào)用one_char_read的時候,就會再次利用read函數(shù)讀取數(shù)據(jù)。read_ptr重新回到數(shù)組首地址,read_cnt中保存的是這次讀取到的字節(jié)數(shù)目。
ssize_t wrap_readline(int fd, void *vptr, size_t maxlen) {ssize_t n, rc;char c, *ptr;ptr = vptr;for(n = 1; n < maxlen; n++){if((rc = one_char_read(fd, &c)) == 1){*ptr++ = c;if(c == '\n')break;}else if(rc == 0){*ptr = 0;return n - 1}elsereturn -1;}//for*ptr = 0;return n; }到這里為止,就可以在client和server中添加錯誤控制了,這時不再直接使用原來的系統(tǒng)調(diào)用,而是使用wrap.c中封裝過的Socket API接口。
2.3.?把client改為交互式輸入
#include <stdio.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #include <arpa/inet.h>#include "wrap.h"#define MAXLINE 80 #define SERV_PORT 8000int main(int argc, char *argv[]) {struct sockaddr_in servaddr;char buf[MAXLINE];int sockfd, n;sockfd = wrap_socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);servaddr.sin_port = htons(SERV_PORT);wrap_connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));while(fgets(buf, MAXLINE, stdin) != NULL){wrap_write(sockfd, buf, strlen(buf));n = wrap_read(sockfd, buf, MAXLINE);if(n == 0)printf("the other side has been closed.\n");elsewrap_write(STDOUT_FILENO, buf, n);}//whilewrap_close(sockfd);return 0; }編譯并運行server和client,看看是否達到了你預想的結(jié)果。
$ ./client haha1 HAHA1 haha2 the other side has been closed. haha3 $這時server仍在運行,但是client的運行結(jié)果并不正確。原因是什么呢?仔細查看server.c可以發(fā)現(xiàn),server對每個請求只處理一次,應答后就關(guān)閉連接,client不能繼續(xù)使用這個連接發(fā)送數(shù)據(jù)。但是client下次循環(huán)時又調(diào)用write發(fā)數(shù)據(jù)給server,write調(diào)用只負責把數(shù)據(jù)交給TCP發(fā)送緩沖區(qū)就可以成功返回了,所以不會出錯,而server收到數(shù)據(jù)后應答一個RST段,client收到RST段后無法立刻通知應用層,只把這個狀態(tài)保存在TCP協(xié)議層。client下次循環(huán)又調(diào)用write發(fā)數(shù)據(jù)給server,由于TCP協(xié)議層已經(jīng)處于RST狀態(tài)了,因此不會將數(shù)據(jù)發(fā)出,而是發(fā)一個SIGPIPE信號給應用層,SIGPIPE信號的缺省處理動作是終止程序,所以看到上面的現(xiàn)象。
為了避免client異常退出,上面的代碼應該在判斷對方關(guān)閉了連接后break出循環(huán),而不是繼續(xù)write。另外,有時候代碼中需要連續(xù)多次調(diào)用write,可能還來不及調(diào)用read得知對方已關(guān)閉了連接就被SIGPIPE信號終止掉了,這就需要在初始化時調(diào)用sigaction處理SIGPIPE信號,如果SIGPIPE信號沒有導致進程異常退出,write返回-1并且errno為EPIPE。
下面修改server,使它可以多次處理同一客戶端的請求:
?
/* wrap_server.c */ #include <stdio.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <ctype.h>#include "wrap.h"#define MAXLINE 80 #define SERV_PORT 8000int main(void) {struct sockaddr_in servaddr, cliaddr;socklen_t cliaddr_len;int listenfd, connfd;char buf[MAXLINE];char str[INET_ADDRSTRLEN];int i, n;listenfd = wrap_socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(SERV_PORT);wrap_bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));wrap_listen(listenfd, 20);printf("Accepting connections ...\n");while(1){cliaddr_len = sizeof(cliaddr);connfd = wrap_accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);while(1){n = wrap_read(connfd, buf, MAXLINE);if(n == 0){printf("the other side has been closed.\n");break;}printf("received from %s at port %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));for(i = 0; i < n; ++i)buf[i] = toupper(buf[i]);wrap_write(connfd, buf, n);}//whilewrap_close(connfd);}//while }經(jīng)過上面的修改后,客戶端和服務器可以進行多次交互了。我們知道,服務器通常是要同時服務多個客戶端的,運行上面的server和client之后,再開一個終端運行client試試,新的client能得到服務嗎?想想為什么。?
使用fork并發(fā)處理多個client的請求
網(wǎng)絡服務器通常用fork來同時服務多個客戶端,父進程專門負責監(jiān)聽端口,每次accept一個新的客戶端連接就fork出一個子進程專門服務這個客戶端。但是子進程退出時會產(chǎn)生僵尸進程,父進程要注意處理SIGCHLD信號和調(diào)用wait清理僵尸進程。
一下給出代碼框架:
listenfd = socket(...); bind(listenfd, ...); listen(listenfd, ...); while (1) {connfd = accept(listenfd, ...);n = fork();if (n == -1) {perror("call to fork");exit(1);} else if (n == 0) {close(listenfd);while (1) {read(connfd, ...);...write(connfd, ...);}close(connfd);exit(0);} elseclose(connfd); }setsockopt
現(xiàn)在做一個測試,首先啟動server,然后啟動client,然后用Ctrl-C使server終止,這時馬上再運行server,結(jié)果是:
$ ./serverbind error: Address already in use這是因為,雖然server的應用程序終止了,但TCP協(xié)議層的連接并沒有完全斷開,因此不能再次監(jiān)聽同樣的server端口。我們用netstat命令查看一下:
$ netstat -apn |grep 8000tcp 1 0 127.0.0.1:33498 127.0.0.1:8000 CLOSE_WAIT 10830/client tcp 0 0 127.0.0.1:8000 127.0.0.1:33498 FIN_WAIT2 -server終止時,socket描述符會自動關(guān)閉并發(fā)FIN段給client,client收到FIN后處于CLOSE_WAIT狀態(tài),但是client并沒有終止,也沒有關(guān)閉socket描述符,因此不會發(fā)FIN給server,因此server的TCP連接處于FIN_WAIT2狀態(tài)。
現(xiàn)在用Ctrl-C把client也終止掉,再觀察現(xiàn)象:
$ netstat -apn |grep 8000tcp 0 0 127.0.0.1:8000 127.0.0.1:44685 TIME_WAIT -$ ./serverbind error: Address already in useclient終止時自動關(guān)閉socket描述符,server的TCP連接收到client發(fā)的FIN段后處于TIME_WAIT狀態(tài)。TCP協(xié)議規(guī)定,主動關(guān)閉連接的一方要處于TIME_WAIT狀態(tài),等待兩個MSL(maximum segment lifetime)的時間后才能回到CLOSED狀態(tài),因為我們先Ctrl-C終止了server,所以server是主動關(guān)閉連接的一方,在TIME_WAIT期間仍然不能再次監(jiān)聽同樣的server端口。MSL在RFC1122中規(guī)定為兩分鐘,但是各操作系統(tǒng)的實現(xiàn)不同,在Linux上一般經(jīng)過半分鐘后就可以再次啟動server了。
在server的TCP連接沒有完全斷開之前不允許重新監(jiān)聽是不合理的,因為,TCP連接沒有完全斷開指的是connfd(127.0.0.1:8000)沒有完全斷開,而我們重新監(jiān)聽的是listenfd(0.0.0.0:8000),雖然是占用同一個端口,但IP地址不同,connfd對應的是與某個客戶端通訊的一個具體的IP地址,而listenfd對應的是wildcard address。解決這個問題的方法是使用setsockopt()設置socket描述符的選項SO_REUSEADDR為1,表示允許創(chuàng)建端口號相同但IP地址不同的多個socket描述符。在server代碼的socket()和bind()調(diào)用之間插入如下代碼:
int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));使用select
select是網(wǎng)絡程序中很常用的一個系統(tǒng)調(diào)用,它可以同時監(jiān)聽多個阻塞的文件描述符(例如多個網(wǎng)絡連接),哪個有數(shù)據(jù)到達就處理哪個,這樣,不需要fork多進程就可以實現(xiàn)并發(fā)服務的server。
?
?
轉(zhuǎn)載于:https://www.cnblogs.com/stemon/p/5212749.html
總結(jié)
以上是生活随笔為你收集整理的linux下socket编程-TCP的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 直播和点播技术分析
- 下一篇: Chrome——我的Chrome插件