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

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Socket套接字通信 TCP UDP详解(网络通信)

發布時間:2023/12/14 编程问答 57 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Socket套接字通信 TCP UDP详解(网络通信) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

    • 一 什么是套接字Socket
      • 1.Socket簡介
      • 2.Socket的域(domain)
      • 3.Socket主要類型(type)
      • 4.Socket基本工作流程
    • 二 創建套接字Socket
      • 1.socket函數
    • 三 綁定套接字Socket與主機網絡地址
      • 1.bind函數
      • 2.struct sockaddr與struct sockaddr_in
      • 3.常用填充地址信息的方法
      • 4.主機字節序與網絡字節序
    • 四 UDP通信的實現
      • 1.recvfrom函數
      • 2.sendto函數
      • 3.示例
    • 五 TCP通信的實現
      • 1.listen函數(server端)
      • 2.accept函數(server端)
      • 3.connect函數(client端)
      • 4.write與read函數
      • 5.send與recv函數
      • 6.示例
    • 六 套接字的緩沖區以及阻塞模式
      • 1.緩沖區
      • 2.使用write/send發送數據
      • 3.使用read/recv讀取數據
    • 七 總結套接字收發數據的過程

一 什么是套接字Socket

1.Socket簡介

所謂套接字(Socket),就是對網絡中不同主機上的應用進程之間進行雙向通信的端點的抽象。一個套接字就是網絡上進程通信的一端,提供了應用層進程利用網絡協議交換數據的機制。從所處的地位來講,套接字上聯應用進程,下聯網絡協議棧,是應用程序通過網絡協議進行通信的接口,是應用程序與網絡協議根進行交互的接口 。

Socket(套接字)可以看成是兩個網絡應用程序進行通信時,各自通信連接中的端點,這是一個邏輯上的概念。它是網絡環境中進程間通信的API(應用程序編程接口),也是可以被命名和尋址的通信端點,使用中的每一個套接字都有其類型和一個與之相連進程。通信時其中一個網絡應用程序將要傳輸的一段信息寫入它所在主機的 Socket中,該 Socket通過與網絡接口卡(NIC)相連的傳輸介質將這段信息送到另外一臺主機的 Socket中,使對方能夠接收到這段信息。 Socket是由IP地址和端口結合的,提供向應用層進程傳送數據包的機制 。

2.Socket的域(domain)

域指定套接字通信中使用的網絡介質。最常見的套接字域是 AF_INET(IPv4)或者AF_INET6(IPV6),它是指 Internet 網絡,許多 Linux 局域網使用的都是該網絡,當然,因特網自身用的也是它。

3.Socket主要類型(type)

  • 流套接字(SOCK_STREAM)
    流套接字用于提供面向連接、可靠的數據傳輸服務。該服務將保證數據能夠實現無差錯、無重復送,并按順序接收。流套接字之所以能夠實現可靠的數據服務,原因在于其使用了傳輸控制協議,即TCP(The Transmission Control Protocol)協議 。
  • 數據報套接字(SOCK_DGRAM)
    數據報套接字提供一種無連接的服務。該服務并不能保證數據傳輸的可靠性,數據有可能在傳輸過程中丟失或出現數據重復,且無法保證順序地接收到數據。數據報套接字使用UDP( User DatagramProtocol)協議進行數據的傳輸。由于數據報套接字不能保證數據傳輸的可靠性,對于有可能出現的數據丟失情況,需要在程序中做相應的處理 。
  • 原始套接字(SOCK_RAW)
    原始套接字與標準套接字(標準套接字指的是前面介紹的流套接字和數據報套接字)的區別在于:原始套接字可以讀寫內核沒有處理的IP數據包,而流套接字只能讀取TCP協議的數據,數據報套接字只能讀取UDP協議的數據。因此,如果要訪問其他協議發送的數據必須使用原始套接 。
  • 4.Socket基本工作流程

    要通過互聯網進行通信,至少需要一對套接字,其中一個運行于客戶端,我們稱之為 Client Socket,另一個運行于服務器端,我們稱之為 Server Socket 。根據連接啟動的方式以及本地套接字要連接的目標,套接字之間的連接過程可以分為三個步驟 :

  • 服務器監聽
    所謂服務器監聽,是指服務器端套接字并不定位具體的客戶端套接字,而是處于等待連接的狀態,實時監控網絡狀態 。
  • 客戶端請求
    所謂客戶端請求,是指由客戶端的套接字提出連接請求,要連接的目標是服務器端的套接字。為此,客戶端的套接字必須首先描述它要連接的服務器的套接字,指出服務器端套接字的地址和端口號,然后就向服務器端接字提出連接請求 。
  • 連接確認
    所謂連接確認,是指當服務器端套接字監聽到或者說接收到客戶端套接字的連接請求,就會響應客戶端套接字的請求,建立一個新的線程,并把服務器端套接字的描述發送給客戶端。一旦客戶端確認了此描述,連接就建立好了。而服務器端套接字繼續處于監聽狀態,接收其他客戶端套接字的連接請求 。
  • 二 創建套接字Socket

    1.socket函數

    int socket(int domain, int type, int protocol); /* 1.函數功能:創建套接字 2.參數:int domain:套接字的域通常為 AF_INET(IPv4)或者AF_INET6(IPV6)int type:套接字類型通常為 SOCK_STREAM、SOCK_DGRAMint protocol:0 :使用默認協議IPPROTO_TCP:使用TCP協議IPPROTO_UDP:使用UDP協議 3.返回值:成功:返回套接字描述符失敗:-1 */

    三 綁定套接字Socket與主機網絡地址

    1.bind函數

    int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); /* 1.函數功能:綁定套接字Socket與主機網絡地址信息 2.參數:int sockfd: 套接字描述符const struct sockaddr *addr:主機地址信息,下文詳解socklen_t addrlen: 參數2的長度(字節) 3.返回值:成功:0失敗:-1 */

    2.struct sockaddr與struct sockaddr_in

    //以下主要摘自LINUX手冊 typedef unsigned short int sa_family_t; /* Structure describing a generic socket address.翻譯:描述通用套接字地址的結構 */ struct sockaddr { sa_family_t sa_family;//地址族char sa_data[14];//14字節,包含套接字中的目標地址和端口信息 }/* Structure describing an Internet socket address.翻譯:描述Internet套接字地址的結構 */ struct sockaddr_in {sa_family_t sin_family; /* address family: AF_INET */in_port_t sin_port; /* port in network byte order */struct in_addr sin_addr; /* internet address */char sin_zero[8];//占位不使用,用來與struct sockaddr對齊}; /* Internet address */ struct in_addr {/*uint32_t*/ in_addr_t s_addr;/* address in network byte order地址的網絡字節序 */};/*sin_addr is the IP host address. The s_addr member of struct in_addr contains the host interface address in network byte order. 翻譯:sin_addr為主機IP地址。struct in_addr的s_addr成員以網絡字節順序包含主機接口地址*/
  • 這兩個結構體一樣大,都是16個字節,而且都有family屬性,二者是并列結構,指向sockaddr_in結構的指針也可以指向sockaddr。不同的是:
    sockaddr結構體中sa_data成員融合了端口與地址信息,而sockaddr_in結構體用兩個成員sin_portsin_addr分別表示端口號和地址信息
  • sin_port和sin_addr都必須是網絡字節序(NBO Network byte order),一般可視化的數字都是主機字節序(HBO Host byte order),下文詳解
  • sockaddr是給操作系統用的。程序員應使用sockaddr_in來表示地址,把類型、ip地址、端口填充sockaddr_in結構體,然后強制轉換成sockaddr,作為參數傳遞給系統調用函數。sockaddr_in用于socket定義和賦值;sockaddr用于函數參數。
  • 示例

    int sockfd; struct sockaddr_in serverAddr; sockfd = socket(AF_INET, SOCK_STREAM, 0);/* 填充struct sockaddr_in */ bzero(&serverAddr, sizeof(serverAddr));//初始化為0狀態 主要是對成員sin_zero[8]清0 serverAddr.sin_family = AF_INET; //設置地址家族 serverAddr.sin_port = htons(SERV_PORT);//端口號1024-65535 serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); /* 強制轉換成struct sockaddr */ bind(sockfd, (struct sockaddr *) &serverAddr, sizeof(serverAddr));

    3.常用填充地址信息的方法

    //填充IP地址 serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);//0.0.0.0 等號后面可以是htonl(0)或者0 serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); inet_aton("127.0.0.1",&serverAddr.sin_addr); inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);//填充端口 serverAddr.sin_port = htons(1234);//端口號1024-65535 serverAddr.sin_port = htons(0);//隨機端口 等號后面可以 0

    相關函數:
    1. inet_addr

    in_addr_t inet_addr(const char *cp); /* 功能:點分字符串格式地址轉網絡格式 參數:IPv4地址字符串例如"127.0.0.1" 返回值:成功:返回網絡字節序的地址用于賦值serverAddr.sin_addr.s_addr失敗:-1 */

    2.inet_ntoa 、inet_aton

    char *inet_ntoa (struct in_addr in) //net to ascii /* 功能:網絡字節序地址轉點分字符串格式地址 參數:傳入通用的網絡字節序地址struct in_addr sin_addr 返回值:成功:返回指針指向IPv4點分字符串格式地址 例如"127.0.0.1"失敗:0 */ int inet_aton(const char *cp, struct in_addr *inp); //ascii to net /* 功能:點分字符串格式地址轉網絡格式地址 參數:cp:IPv4點分字符串格式地址inp:網絡字節序地址struct in_addr sin_addr 返回值:成功:非0失敗:0 */

    4.htons、htonl

    uint16_t htons(uint16_t hostshort);//h host n net s short uint32_t htonl(uint32_t hostlong);//h host n net l long /* 功能:將主機字節序的short/long類型數據轉為網絡字節序類型數據 參數:short類型數據/long類型 返回值:成功:網絡字節序類型數據失敗:-1 */

    5.inet_ptoninet_ntop
    這兩個函數是隨IPv6出現的函數,對于IPv4地址和IPv6地址都適用,函數中p和n分別代表表達(presentation)和數值(numeric)。地址的表達格式通常是ASCII字符串,數值格式則是存放到套接字地址結構的二進制值。

    int inet_pton(int family, const char *strptr, void *addrptr); //返回值:若成功則為1,若輸入不是有效的表達式則為0,若出錯則為-1 const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len); //返回值:若成功則為指向結構的指針,若出錯則為NULL

    4.主機字節序與網絡字節序

    NBO : 網絡字節序
    HBO : 主機字節序
    LE little-endian:小端
    BE big-endian:大端

  • 網絡字節序和主機字節序:
    網絡數據流的地址規定:先發出的數據是低地址,后發出的數據是高地址。
    發送主機通常將發送緩沖區中的數據按內存地址從低到高的順序發出,為了不使數據流亂序,接收主機也會把從網絡上接收的數據按內存地址從低到高的順序保存在接收緩沖區中。
    TCP/IP協議規定:網絡數據流應采用大端字節序,即低地址高字節。
  • tcp/ip規定它們的網絡字節序都是大端字節序。主機字節序可能是大端也可能是小端,與主機的cpu有關,與操作系統無關考慮到與協議的一致以及與同類其它平臺產品的互通,在程序中發數據包時,將主機字節序轉換為網絡字節序,收數據包處將網絡字 節序轉換為主機字節序。網絡程序開發時 或是跨平臺開發時 應該注意保證只用一種字節序 不然兩方的解釋不一樣就會產生bug。數據在傳輸的過程中,一定有一個標準化的過程,也就是說:
    從主機a到主機b進行通信:a的主機字節序——網絡字節序——b的主機字節序

  • 大端字節序和小端字節序:
    大端字節序存儲時值的高位存儲在較小的地址,值的低位存儲在較大的地址。
    小端字節序存儲時值的高位存儲在較大的地址,值的低位存儲在較小的地址。
    以0x12345678為例:
    地址:0x1000 ?0x1001? 0x1002 ?0x1003
    小端: 78 ???56 ???34 ???12
    大端: 12 ???34 ???56 ???78
  • 測試主機是大端還是小端的方法:
  • int main() {union{short s;char c[sizeof(short)];}un;un.s = 0x0102;if(sizeof(short)==2){if(un.c[0] == 1 && un.c[1] == 2)printf("Big-Endian\n");else if(un.c[0] == 2 && un.c[1] == 1)printf("Little-Endian\n");elseprintf("Unknown\n");}elseprint("sizeof(short)=%d\n",sizeof(short));exit(0); }

    四 UDP通信的實現

    在創建并綁定套接字之后,我們就可以嘗試TCP、UDP通信了。
    TCP/IP協議是一個協議簇。里面包括很多協議,UDP只是其中的一個。

    • UDP(User Datagram Protocol用戶數據報協議)是一個非連接的協議,傳輸數據之前源端和終端不建立連接, 當它想傳送時就簡單地去抓取來自應用程序的數據,并盡可能快地把它扔到網絡上。 在發送端,UDP傳送數據的速度僅僅是受應用程序生成數據的速度、 計算機的能力和傳輸帶寬的限制; 在接收端,UDP把每個消息段放在隊列中,應用程序每次從隊列中讀一個消息段。
    • UDP 是不具有可靠性的數據報協議。細微的處理它會交給上層的應用去完成。在 UDP 的情況下,雖然可以確保發送消息的大小,卻不能保證消息一定會到達。因此,應用有時會根據自己的需要進行重發處理。

    1.recvfrom函數

    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen); /* 功能:接收數據 參數:int sockfd:socket函數的返回值,套接字描述符void *buf:存放收到的數據size_t len:參數2的大小int flags:如果沒有數據到來 阻塞等待還是不等待 0表示阻塞 MSG_DONTWAIT 不等待struct sockaddr *src_addr:用于獲取發送方的地址信息socklen_t *addrlen:發送方地址信息長度 注意:傳的實參必須初始化 返回值:成功:返回實際收到的字節數失敗:-1 */

    2.sendto函數

    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen); /* 功能:發送數據給對端 參數:int sockfd:socket函數的返回值,套接字描述符const void *buf:要發送的數據存放的地址size_t len:參數2的大小int flags:套接字緩存滿 阻塞還是不阻塞 0表示阻塞 MSG_DONTWAIT 不阻塞const struct sockaddr *dest_addr:目標端的地址信息socklen_t *addrlen:目標端的地址信息 返回值:成功:返回實際發送的字節數失敗:-1 */

    3.示例

    實現服務器端與客戶端聊天
    運行效果

    /***************************/ /* 服務器端 */ /***************************/ #include <stdio.h> #include <strings.h> #include <sys/types.h> #include <unistd.h> #include <errno.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h>int main() {char buf_data[1024] = {};/*創建套接字*/int sockfd = socket(AF_INET,SOCK_DGRAM,0);if(sockfd == -1){perror("socket");exit(1);}printf("sockfd:%d\n",sockfd);/*定義網絡地址結構體變量并填充*/struct sockaddr_in myselfAddr;myselfAddr.sin_family = AF_INET;myselfAddr.sin_port = htons(6666);//把短整形轉為網絡格式myselfAddr.sin_addr.s_addr = htonl(INADDR_ANY);//主機格式轉網絡格式/*套接字與主機綁定*/int ret_bind = bind(sockfd,(struct sockaddr*)&myselfAddr,sizeof(myselfAddr));if(ret_bind == -1){perror("bind");close(sockfd);exit(1);}/*緩存用于獲取對端網絡地址信息*/struct sockaddr_in buf_sockaddr;socklen_t buf_addrlen = sizeof(buf_sockaddr);printf("等待客戶端連接...\n");ssize_t ret_recv = recvfrom(sockfd,buf_data,sizeof(buf_data),0,(struct sockaddr*)&buf_sockaddr,&buf_addrlen);if(ret_recv == -1){perror("recvfrom");close(sockfd);exit(1);}printf("IP:%s:%s\n",inet_ntoa(buf_sockaddr.sin_addr),buf_data);pid_t pid = fork();if(pid>0){while(1){bzero(buf_data,sizeof(buf_data));gets(buf_data);ssize_t ret_send = sendto(sockfd,buf_data,strlen(buf_data)+1,0,(struct sockaddr*)&buf_sockaddr,buf_addrlen);if(ret_send == -1){perror("sendto");close(sockfd);exit(1);}printf("我:%s\n",buf_data);}}else if(pid == 0){while(1){ssize_t ret_recv = recvfrom(sockfd,buf_data,sizeof(buf_data),0,(struct sockaddr*)&buf_sockaddr,&buf_addrlen);if(ret_recv == -1){perror("recvfrom");close(sockfd);exit(1);}printf("IP:%s:%s\n",inet_ntoa(buf_sockaddr.sin_addr),buf_data);}}else{perror("fork");close(sockfd);exit(1);}return 0; }/***************************/ /* 客戶端 */ /***************************/ #include <stdio.h> #include <strings.h> #include <sys/types.h> #include <unistd.h> #include <errno.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h>int main() {char buf_data[1024] = "\0";/*創建套接字*/int sockfd = socket(AF_INET,SOCK_DGRAM,0);if(sockfd == -1){perror("socket");exit(1);}printf("sockfd:%d\n",sockfd);/*輸入并配置對端網絡地址信息*/short port;char IP[20];printf("輸入對方IP:\n");scanf("%s",IP);getchar();printf("輸入對方端口號:\n");scanf("%hd",&port);getchar();struct sockaddr_in serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(port);//短整型轉為網絡格式serverAddr.sin_addr.s_addr = inet_addr(IP);//字符串格式轉網絡地址格式/*緩存用于獲取對端的網絡地址信息*/struct sockaddr_in buf_sockaddr;socklen_t buf_addrlen = sizeof(buf_sockaddr);pid_t pid = fork();if(pid>0){while(1){/*發送信息*/bzero(buf_data,sizeof(buf_data));gets(buf_data);ssize_t ret_send = sendto(sockfd,buf_data,strlen(buf_data)+1,0,(struct sockaddr*)&serverAddr,sizeof(serverAddr));if(ret_send == -1){perror("sendto");close(sockfd);exit(1);} printf("我:%s\n",buf_data);bzero(buf_data,sizeof(buf_data));}}else if(pid == 0){/*接收信息*/while(1){bzero(buf_data,sizeof(buf_data));ssize_t ret_recv = recvfrom(sockfd,buf_data,sizeof(buf_data),0,(struct sockaddr*)&buf_sockaddr,&buf_addrlen);if(ret_recv == -1){perror("recvfrom");close(sockfd);exit(1);}printf("IP:%s:%s\n",inet_ntoa(buf_sockaddr.sin_addr),buf_data);}}else{perror("fork");close(sockfd);exit(1);}return 0; }

    五 TCP通信的實現

    • TCP(Transmission Control Protocol,傳輸控制協議)是面向連接的協議,也就是說,在收發數據前,必須和對方建立可靠的連接。
    • TCP 是面向連接的、可靠的流協議。流就是指不間斷的數據結構,當應用程序采用 TCP 發送消息時,雖然可以保證發送的順序,但還是猶如沒有任何間隔的數據流發送給接收端。TCP 為提供可靠性傳輸,實行“順序控制”或“重發控制”機制。此外還具備“流控制(流量控制)”、“擁塞控制”、提高網絡利用率等眾多功能。

    1.listen函數(server端)

    • 對于服務器端程序,使用bind 函數綁定套接字后,還需要使用listen 函數讓套接字進入被動監聽狀態,再調用accept 函數,就可以隨時響應客戶端的請求了。
    • 所謂被動監聽,是指當沒有客戶端請求時,套接字處于“睡眠”狀態,只有當接收到客戶端請求時,套接字才會被“喚醒”來響應請求。
    int listen(int sockfd, int backlog); /* 功能:使套接字進入被動監聽狀態 參數:int sockfd: 需要進入監聽狀態的套接字int backlog:請求隊列的最大長度 返回值:成功:0失敗:-1 */
    • 當套接字正在處理客戶端請求時,如果有新的請求進來,套接字是沒法處理的,只能把它放進緩沖區,待當前請求處理完畢后,再從緩沖區中讀取出來處理。如果不斷有新的請求進來,它們就按照先后順序在緩沖區中排隊,直到緩沖區滿。這個緩沖區,就稱為 請求隊列(Request Queue)
    • 當請求隊列滿時,就不再接收新的請求,對于 Linux,客戶端會收到 ECONNREFUSED 錯誤
    • listen只是讓套接字處于監聽狀態,并沒有接收請求。接收請求需要使用 accept函數

    2.accept函數(server端)

    • 當套接字處于監聽狀態時,可以通過 accept函數來接收客戶端請求。
    • listen只是讓套接字進入監聽狀態,并沒有真正接收客戶端請求,listen后面的代碼會繼續執行,直到遇到 accept
    • accept 會阻塞程序執行,直到有新的請求到來。
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); /* 功能:處理來自客戶端的連接請求 參數:int sockfd: 處于監聽狀態的套接字(服務器綁定的套接字也叫監聽套接字)struct sockaddr *addr:用于獲取對端的地址信息socklen_t *addrlen:參數2的大小 返回值:成功:返回一個新的套接字(這個套接字與當前發起申請的客戶端連接)失敗:-1 */

    3.connect函數(client端)

    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); /* 功能:客戶端發起連接服務器請求 參數:int sockfd:client套接字const struct sockaddr *addr:對端(服務器)的地址信息socklen_t *addrlen:參數2的大小 返回值:成功:0失敗:-1 */

    4.write與read函數

    建立好了 TCP 連接之后,我們就可以把得到的 sockfd 當作文件描述符來使用。

    #include <unistd.h> ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count);

    需要注意 read 函數的返回值:

    • retval > 0 :實際讀到的字節數
    • retval = 0 :
      ?????普通文件 — 到達文件末尾
      ?????管道文件 — 管道寫端關閉
      ?????套接字文件 — 對端關閉,網絡斷開
    • retval < 0 :出錯

    5.send與recv函數

    recv 和 send 函數提供了和 read 和 write 差不多的功能。前3個參數同read、write,第4
    個參數用來控制讀寫操作。

    #include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags); /* 參數 int flags:0:等同于writeMSG_DONTROUTE:告訴 IP 目的主機在本地網絡上面,沒有必要查找表,這個標志一般用網絡診斷和路由程序里面MSG_OOB:表示可以接收和發送帶外的數據MSG_DONTWAIT:僅本操作非阻塞(執行完恢復阻塞模式) */ ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags); /* 參數 int flags:0:等同于readMSG_PEEK:表示只是從系統緩沖區中讀取內容,而不清除系統緩沖區的內容MSG_OOB:表示可以接收和發送帶外的數據MSG_DONTWAIT:僅本操作非阻塞(執行完恢復阻塞模式)MSG_WAITALL:表示等到所有的信息到達時才返回。使用這個標志的時候 recv 會一直阻塞,直到指定的條件滿足,或者是發生了錯誤:1.當讀到了指定的字節時,函數正常返回。返回值等于 len2.當讀到了文件的結尾時,函數正常返回。返回值小于 len3.當操作發生錯誤時,返回 -1,且設置錯誤為相應的錯誤號 (errno) */

    6.示例

    實現客戶端與服務器端的對話,服務器端運行時命令行傳參端口號,客戶端運行時命令行傳參服務器的IP和端口號
    關于程序中用到的IO多路復用select函數,參考select函數詳解

    運行效果

    /***************************/ /* 服務器端 */ /***************************/ #include <stdio.h> #include <strings.h> #include <sys/types.h> #include <unistd.h> #include <errno.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/select.h>void error_Handling(char* func,int retval); void error_of_read(int retval,char* IP); void sigFun(int sig); int count = 0;int main(int argc,char* argv[]) {if(argc != 2){printf("%s Port",argv[0]);exit(1);}signal(SIGCHLD,sigFun);char buf_data[1024] = {};/*創建套接字——監聽套接字*/int listenfd = socket(AF_INET,SOCK_STREAM,0);error_Handling("socket",listenfd);/*綁定監聽套接字與主機網絡地址信息*/int on = 1;int ret_set = setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));//地址復用error_Handling("setsockopt",ret_set);struct sockaddr_in serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(atoi(argv[1]));serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);int ret_bind = bind(listenfd,(struct sockaddr*)&serverAddr,sizeof(serverAddr));error_Handling("bind",ret_bind);/*設置監聽隊列的大小*/int ret_listen = listen(listenfd,10);error_Handling("listen",ret_listen);/*監聽等待連接*/struct sockaddr_in buf_addr;socklen_t buf_addrlen = sizeof(buf_addr);while(1){printf("服務器持續監聽中\n");printf("連接服務器的客戶端數量:%d\n",count++);int newconfd = accept(listenfd,(struct sockaddr*)&buf_addr,&buf_addrlen);//返回分機套接字error_Handling("accept",newconfd);pid_t pid = fork();error_Handling("fork",pid);if(pid == 0){printf("FATHERPID:%d CHILDPID:%d\n",getppid(),getpid());printf("與IP:| %s |建立連接\n",inet_ntoa(buf_addr.sin_addr));while(1){/*在這里做一個IO多路復用*/fd_set readfds;FD_ZERO(&readfds);FD_SET(0,&readfds);FD_SET(newconfd,&readfds);int ret_select = select(newconfd+1,&readfds,NULL,NULL,NULL);error_Handling("select",ret_select);/*select返回表示有描述符就緒*/if(FD_ISSET(0,&readfds))//檢查標準輸入是否被置位{ssize_t ret_read = read(0,&buf_data,sizeof(buf_data));error_Handling("read",ret_read);ssize_t ret_write = write(newconfd,&buf_data,sizeof(buf_data));error_Handling("write",ret_write);printf("我:%s\n",buf_data);bzero(&buf_data,sizeof(buf_data));}if(FD_ISSET(newconfd,&readfds))//檢查套接字是否被置位{ssize_t ret_read = read(newconfd,&buf_data,sizeof(buf_data));error_of_read(ret_read,inet_ntoa(buf_addr.sin_addr));printf("%s:%s\n",inet_ntoa(buf_addr.sin_addr),buf_data);bzero(&buf_data,sizeof(buf_data));}}}}/*關閉套接字*///close(newconfd);close(listenfd);return 0; } void error_Handling(char* func,int retval) {if(retval == -1){perror(func);exit(1);} } void error_of_read(int retval,char* IP) {if(retval<0){perror("read");exit(1);}if(retval == 0){perror("read");printf("%s 已斷開連接,此進程結束\n",IP);exit(1);} } void sigFun(int sig) {wait(NULL);count--;printf("有子進程退出已經收尸\n"); }/***************************/ /* 客戶端 */ /***************************/ #include <stdio.h> #include <strings.h> #include <sys/types.h> #include <unistd.h> #include <errno.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/select.h>void error_Handling(char* func,int retval);int main(int argc,char* argv[]) {if(argc != 3){printf("%s IP Port\n",argv[0]);exit(1);}char buf_data[1024] = {};/*創建套接字*/int sockfd = socket(AF_INET,SOCK_STREAM,0);error_Handling("socket",sockfd);/*連接*/struct sockaddr_in serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(atoi(argv[2]));serverAddr.sin_addr.s_addr = inet_addr(argv[1]);int ret_connect = connect(sockfd,(struct sockaddr*)&serverAddr,sizeof(serverAddr));error_Handling("connect",ret_connect);/*發送數據*/while(1){/*在這里做一個IO多路復用*/fd_set readfds;FD_ZERO(&readfds);FD_SET(0,&readfds);FD_SET(sockfd,&readfds);int ret_select = select(sockfd+1,&readfds,NULL,NULL,NULL);error_Handling("select",ret_select);/*select返回表示有描述符就緒*/if(FD_ISSET(0,&readfds))//檢查標準輸入是否被置位{ssize_t ret_read = read(0,&buf_data,sizeof(buf_data));error_Handling("read",ret_read);ssize_t ret_write = write(sockfd,&buf_data,sizeof(buf_data));error_Handling("write",ret_write);printf("我:%s\n",buf_data);bzero(&buf_data,sizeof(buf_data));}if(FD_ISSET(sockfd,&readfds))//檢查套接字是否被置位{ssize_t ret_read = read(sockfd,&buf_data,sizeof(buf_data));error_Handling("read",ret_read);printf("%s:%s\n",inet_ntoa(serverAddr.sin_addr),buf_data);bzero(&buf_data,sizeof(buf_data));}}/*關閉套接字*/close(sockfd);return 0; }void error_Handling(char* func,int retval) {if(retval == -1){perror(func);exit(1);} }

    六 套接字的緩沖區以及阻塞模式


    參考socket套接字及緩沖區詳解

    1.緩沖區

    • 每個 socket 被創建后,都會分配兩個緩沖區,輸入緩沖區輸出緩沖區
    • write()/send()/send to() 函數并不立即向網絡中傳輸數據,而是先將數據寫入緩沖區中,再由TCP協議將數據從緩沖區發送到目標機器。一旦將數據寫入到緩沖區,函數就可以成功返回,不管它們有沒有到達目標機器,也不管它們何時被發送到網絡,這些都是TCP協議負責的事情。
    • read()/recv()/recefrom() 函數也是如此,也從輸入緩沖區中讀取數據,而不是直接從網絡中讀取。
    • 每個套接字的I/O緩沖區單獨存在。
    • 即使一端關閉套接字,也會繼續傳送這端套接字輸出緩沖區中遺留的數據。
    • 如果一端關閉套接字,這端將丟失輸入緩沖區中的數據。
    • 默認情況下,套接字為阻塞模式
    //把套接字設置成非阻塞模式 fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); //非阻塞模式的使用并不普遍,因為非阻塞模式會浪費大量的CPU資源

    2.使用write/send發送數據

    阻塞模式下:

    • 首先會檢查輸出緩沖區,如果緩沖區的可用空間長度小于要發送的數據,那么 write()/send() 會被阻塞(暫停執行),直到緩沖區中的數據被發送到目標機器,騰出足夠的空間,才喚醒 write()/send() 函數繼續寫入數據;
    • 如果要寫入的數據大于緩沖區的最大長度,那么將分批寫入。直到所有數據被寫入緩沖區 write()/send() 才能返回。
    • 如果TCP協議正在向網絡發送數據,那么輸出緩沖區會被鎖定,不允許寫入,write()/send() 也會被阻塞,直到數據發送完畢緩沖區解鎖,write()/send() 才會被喚醒。
    • send()函數默認情況下會使用Nagle算法。Nagle算法通過將未確認的數據存入緩沖區直到積攢到一定數量一起發送的方法,來降低主機發送零碎小數據包的數目。所以假設send()函數發送數據過快的話,該算法會將一些數據打包后統一發出去。通過setsockopt()的TCP_NODELAY選項來禁用Nagle算法。

    非阻塞模式下:

    • write/send不做等待立即返回;
    • write()/send()函數的過程僅僅是將數據拷貝到協議棧的緩沖區而已,如果緩沖區可用空間不夠,則盡可能拷貝,返回成功拷貝的大小;
    • 如果緩存區可用空間為0,則返回-1,同時設置errno為EWOULDBLOCK。

    3.使用read/recv讀取數據

    read/recv函數返回其實際copy的字節數,如果recv在copy時出錯,那么它返回SOCKET_ERROR;如果recv函數在等待協議接收數據時網絡中斷了,那么它返回0。
    阻塞模式下:

    • 首先會檢查輸入緩沖區,如果緩沖區中有數據,那么就讀取,否則函數會被阻塞,直到網絡上有數據到來;(如果數據正在從輸入緩沖區拷貝到用戶空間,read/recv也會被阻塞)
    • 如果要讀取的數據長度小于緩沖區中的數據長度,那么就不能一次性將緩沖區中的所有數據讀出,剩余數據將不斷積壓,直到輸入緩沖區滿,協議棧不能再接收數據。

    非阻塞模式下:

    • write/send不做等待立即返回;
    • 成功返回實際讀到的字節數;
    • 如果輸入緩沖區中沒有數據,返回錯誤EWOULDBLOCK。

    七 總結套接字收發數據的過程

    TCP發送數據的過程:首先,TCP是有鏈接的可靠傳輸協議,所謂可靠也就是說保證客戶端發送的數據服務端都能夠收到,并且是按序收到。

  • 數據首先由應用程序緩沖區復制到發送端的輸出緩沖區(位于內核),注意這個過程是用類似write功能的函數完成的。有的人通常看到write成功就以為數據發送到了對端主機,其實這是錯誤的,write成功僅僅表示數據成功的由應用進程緩沖區復制到了輸出緩沖區。

  • 然后內核協議棧將輸出緩沖區中的數據發送到對端主機,注意這個過程不受應用程序控制,而是發送端內核協議棧完成,其中包括使用滑動窗口、擁塞控制等功能。

  • 數據到達接收端主機的輸入緩沖區,注意這個接收過程也不受應用程序控制,而是由接收端內核協議棧完成,其中包括發送ack確認等。

  • 數據由套接字接收緩沖區復制到接收端應用程序緩沖區,注意這個過程是由類似read等函數來完成。

  • 思考:如果TCP服務端一直sleep,客戶端一直發送數據,會出現什么情況?

  • 阻塞模式下:
    如果服務端一直sleep不接收數據,而客戶端一直write,也就是只能執行上述過程中的前三步,這樣最終結果肯定是接收端的輸入緩沖區和發送端的輸出緩沖區都被填滿,這樣write就無法繼續將數據從應用程序復制到發送端的輸出緩沖區了,從而使進程進入睡眠。
  • 非阻塞情況下:
    服務端一直sleep,隨著客戶端write,接收端的輸入緩沖區和發送端的輸出緩沖區會被填滿。當發送端的輸出緩沖區的可用空間為0時,write立即返回-1,并將errno置為EWOULDBLOCK。
  • “華清遠見” http://www.hqyj.com/學習更多編程知識

    總結

    以上是生活随笔為你收集整理的Socket套接字通信 TCP UDP详解(网络通信)的全部內容,希望文章能夠幫你解決所遇到的問題。

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