日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > linux >内容正文

linux

linux下socket编程-TCP

發布時間:2025/3/19 linux 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 linux下socket编程-TCP 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

網絡字節序

發送主機通常將發送緩沖區中的數據按內存地址從低到高的順序發出,接收主機把從網絡上接到的字節依次保存在接收緩沖區中,也是按內存地址從低到高的順序保存,因此,網絡數據流的地址應這樣規定:先發出的數據是低地址,后發出的數據是高地址

為使網絡程序具有可移植性,使同樣的C代碼在大端和小端計算機上編譯后都能正常運行,可以調用以下庫函數做網絡字節序和主機字節序的轉換。

#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地址轉換后準備發送。如果主機是小端字節序,這些函數將參數做相應的大小端轉換然后返回,如果主機是大端字節序,這些函數不做轉換,將參數原封不動地返回。 

Socket地址的數據類型 

具體細節:

sockaddr的缺陷:sa_data把目標地址和端口信息混在一起了。

sockaddr_in結構體解決了sockaddr的缺陷,把port和addr 分開儲存在兩個變量中。

對于struct in_addr還有另一種形式的實現:

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字節,32位,按照網絡字節順序存儲IP地址} S_un; };

只要取得某種sockaddr結構體的首地址,不需要知道具體是哪種類型的sockaddr結構體,就可以根據地址類型字段確定結構體中的內容。因此,socket API可以接受各種類型的sockaddr結構體指針做參數,例如bind、accept、connect等函數,這些函數的參數應該設計成void *類型以便接受各種類型的指針,但是sock API的實現早于ANSI C標準化,那時還沒有void *類型,因此這些函數的參數都用struct sockaddr *類型表示,在傳遞參數之前要強制類型轉換一下,例如:

struct sockaddr_in servaddr; /* initialize servaddr */ bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));

sockaddr_in中的成員struct in_addr sin_addr表示32位的IP地址。但是我們通常用點分十進制的字符串表示IP地址,以下函數可以在字符串表示和in_addr表示之間轉換。

字符串轉in_addr的函數:

#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轉字符串的函數:

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不僅可以轉換IPv4的in_addr,還可以轉換IPv6的in6_addr,因此函數接口是void *addrptr。

TCP協議通信流程

如果客戶端沒有更多的請求了,就調用close()關閉連接,就像寫端關閉的管道一樣,服務器的read()返回0,這樣服務器就知道客戶端關閉了連接,也調用close()關閉連接。注意,任何一方調用close()后,連接的兩個傳輸方向都關閉,不能再發送數據了。如果一方調用shutdown()則連接處于半關閉狀態,仍可接收對方發來的數據。

提示:read()返回0就表明收到了FIN段。

最簡單的TCP網絡程序

/*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,唯一標識網絡通信的一個進程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}

服務器的網絡地址為INADDR_ANY,這個宏表示本地的任意IP地址,因為服務器可能有多個網卡,每個網卡也可能綁定多個IP地址,這樣設置可以在所有的IP地址上監聽,直到與某個客戶端建立了連接時才確定下來到底用哪個IP地址,端口號為SERV_PORT,我們定義為8000。

在accept函數中,cliaddr是一個傳出參數,accept()返回時傳出客戶端的地址和端口號。addrlen參數是一個傳入傳出參數(value-result argument),傳入的是調用者提供的緩沖區cliaddr的長度以避免緩沖區溢出問題,傳出的是客戶端地址結構體的實際長度(有可能沒有占滿調用者提供的緩沖區)。如果給cliaddr參數傳NULL,表示不關心客戶端的地址。

/* 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程序監聽8000端口,IP地址還沒確定下來。

現在編譯運行客戶端:

$ ./client abcd Response from server: ABCD

回到server所在的終端,看看server的輸出:

$ ./serverAccepting connections ...received from 127.0.0.1 at PORT 59757

再做一個小實驗,在客戶端的connect()代碼之后插一個while(1);死循環,使客戶端和服務器都處于連接中的狀態,用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連接。

錯誤處理與讀寫控制

系統調用不能保證每次都成功,必須進行出錯處理,這樣一方面可以保證程序邏輯正常,另一方面可以迅速得到故障信息。

為使錯誤處理的代碼不影響主程序的可讀性,我們把與socket相關的一些系統函數加上錯誤處理代碼包裝成新的函數,做成一個模塊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"); }

慢系統調用accept、read和write被信號中斷時應該重試。connect雖然也會阻塞,但是被信號中斷時不能立刻重試。對于accept,如果errno是ECONNABORTED,也應該重試。

TCP協議是面向流的,read和write調用的返回值往往小于參數指定的字節數。對于read調用,如果接收緩沖區中有20字節,請求讀100個字節,就會返回20。對于write調用,如果請求寫100個字節,而發送緩沖區中只有20個字節的空閑位置,那么write會阻塞,直到把100個字節全部交給發送緩沖區才返回,但如果socket文件描述符有O_NONBLOCK標志,則write不阻塞,直接返回20。為避免這些情況干擾主程序的邏輯,確保讀寫我們所請求的字節數,我們實現了兩個包裝函數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函數返回了負數,那么這個負數的絕對值就表示多讀取了多少的字節數。

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; }

如果應用層協議的各字段長度固定,用readn來讀是非常方便的。例如設計一種客戶端上傳文件的協議,規定前12字節表示文件名,超過12字節的文件名截斷,不足12字節的文件名用'\0'補齊,從第13字節開始是文件內容,上傳完所有文件內容后關閉連接,服務器可以先調用readn讀12個字節,根據文件名創建文件,然后在一個循環中調用read讀文件內容并存盤,循環結束的條件是read返回0。

字段長度固定的協議往往不夠靈活,難以適應新的變化。如果新版本的協議要添加新的字段,比如規定前12字節是文件名,從13到16字節是文件類型說明,從第17字節開始才是文件內容,同樣會造成和老版本的程序無法兼容的問題。

現在看一下TFTP協議是如何避免上述問題的:TFTPTFTP協議的各字段是可變長的,以'\0'為分隔符,文件名可以任意長,這樣,以后添加新的選項仍然可以和老版本的程序兼容(老版本的程序只要忽略不認識的選項就行了)。因此,常見的應用層協議都是帶有可變長字段的,字段之間的分隔符用換行的比用'\0'的更常見,例如本節后面要介紹的HTTP協議。可變長字段的協議用readn來讀就很不方便了,為此我們實現一個類似于fgets的readline函數,也放在wrap.c中:

//每次調用one_char_read返回一個字節數據 //字節數據是暫存在靜態的數組中的 static ssize_t one_char_read(int fd, char *ptr) {static int read_cnt;static char *read_ptr; //靜態緩沖區的遍歷指針static char read_buf[100]; //靜態數據緩沖區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; }

當把靜態緩沖區的字節全部返回后,read_cnt=0,下次在調用one_char_read的時候,就會再次利用read函數讀取數據。read_ptr重新回到數組首地址,read_cnt中保存的是這次讀取到的字節數目。

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中添加錯誤控制了,這時不再直接使用原來的系統調用,而是使用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,看看是否達到了你預想的結果。

$ ./client haha1 HAHA1 haha2 the other side has been closed. haha3 $

這時server仍在運行,但是client的運行結果并不正確。原因是什么呢?仔細查看server.c可以發現,server對每個請求只處理一次,應答后就關閉連接,client不能繼續使用這個連接發送數據。但是client下次循環時又調用write發數據給server,write調用只負責把數據交給TCP發送緩沖區就可以成功返回了,所以不會出錯,而server收到數據后應答一個RST段,client收到RST段后無法立刻通知應用層,只把這個狀態保存在TCP協議層。client下次循環又調用write發數據給server,由于TCP協議層已經處于RST狀態了,因此不會將數據發出,而是發一個SIGPIPE信號給應用層,SIGPIPE信號的缺省處理動作是終止程序,所以看到上面的現象。

為了避免client異常退出,上面的代碼應該在判斷對方關閉了連接后break出循環,而不是繼續write。另外,有時候代碼中需要連續多次調用write,可能還來不及調用read得知對方已關閉了連接就被SIGPIPE信號終止掉了,這就需要在初始化時調用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 }

經過上面的修改后,客戶端和服務器可以進行多次交互了。我們知道,服務器通常是要同時服務多個客戶端的,運行上面的server和client之后,再開一個終端運行client試試,新的client能得到服務嗎?想想為什么。?

使用fork并發處理多個client的請求

網絡服務器通常用fork來同時服務多個客戶端,父進程專門負責監聽端口,每次accept一個新的客戶端連接就fork出一個子進程專門服務這個客戶端。但是子進程退出時會產生僵尸進程,父進程要注意處理SIGCHLD信號和調用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

現在做一個測試,首先啟動server,然后啟動client,然后用Ctrl-C使server終止,這時馬上再運行server,結果是:

$ ./serverbind error: Address already in use

這是因為,雖然server的應用程序終止了,但TCP協議層的連接并沒有完全斷開,因此不能再次監聽同樣的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描述符會自動關閉并發FIN段給client,client收到FIN后處于CLOSE_WAIT狀態,但是client并沒有終止,也沒有關閉socket描述符,因此不會發FIN給server,因此server的TCP連接處于FIN_WAIT2狀態。

現在用Ctrl-C把client也終止掉,再觀察現象:

$ netstat -apn |grep 8000tcp 0 0 127.0.0.1:8000 127.0.0.1:44685 TIME_WAIT -$ ./serverbind error: Address already in use

client終止時自動關閉socket描述符,server的TCP連接收到client發的FIN段后處于TIME_WAIT狀態。TCP協議規定,主動關閉連接的一方要處于TIME_WAIT狀態,等待兩個MSL(maximum segment lifetime)的時間后才能回到CLOSED狀態,因為我們先Ctrl-C終止了server,所以server是主動關閉連接的一方,在TIME_WAIT期間仍然不能再次監聽同樣的server端口。MSL在RFC1122中規定為兩分鐘,但是各操作系統的實現不同,在Linux上一般經過半分鐘后就可以再次啟動server了。

在server的TCP連接沒有完全斷開之前不允許重新監聽是不合理的,因為,TCP連接沒有完全斷開指的是connfd(127.0.0.1:8000)沒有完全斷開,而我們重新監聽的是listenfd(0.0.0.0:8000),雖然是占用同一個端口,但IP地址不同,connfd對應的是與某個客戶端通訊的一個具體的IP地址,而listenfd對應的是wildcard address。解決這個問題的方法是使用setsockopt()設置socket描述符的選項SO_REUSEADDR為1,表示允許創建端口號相同但IP地址不同的多個socket描述符。在server代碼的socket()和bind()調用之間插入如下代碼:

int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

使用select

select是網絡程序中很常用的一個系統調用,它可以同時監聽多個阻塞的文件描述符(例如多個網絡連接),哪個有數據到達就處理哪個,這樣,不需要fork多進程就可以實現并發服務的server。

?

  

  

?

轉載于:https://www.cnblogs.com/stemon/p/5212749.html

總結

以上是生活随笔為你收集整理的linux下socket编程-TCP的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。