二、TCP網絡編程


隨着網絡的發展,網絡通信必不可少,整體網絡的實現是採取分層的方法實現的。應用層是對於要發送的數據的一種控制;傳輸層是兩個進程間的通信,實現如何傳數據。傳輸層協議:

  • TCP:面向連接的,可靠的,字節流服務
  • UDP:無連接的,不可靠的,數據報服務

今天我們學習傳輸層的TCP協議編程流程。

一、基本概念

TCP編程函數中的某些函數中的參數涉及到一些其他概念,爲了理解函數不喫力,我們先看一些基礎概念。

小知識點:

  1. errno 是記錄系統的最後一次錯誤代碼,是一個int型的值。
  2. 命令: uname -a。作用: 查看系統內核版本號及系統名稱

(一)通用socket地址

socket含義是一個IP地址和端口,唯一標識了使用TCP通信的一端,一般稱爲套接字或socket地址。

socket網絡編程接口中表示socket地址的是結構體sockaddr,其定義如下:

# include <bits/socket.h>
struct sockaddr
{
     sa_family_t sa_family;//地址族類型變量
     char sa_data[14];//存放socket地址值
};

sa_family成員是地址族類型變量,地址族類型通常與協議族類型對應。常用的協議族(protocol family也稱爲domain)和對應的地址族的關係如下表

協議族 地址族 描述
PF_UNIX AF_UNIX UNIX本地域協議族,長度可達到108字節
PF_INET AF_INET TCP/IPv4協議族,6字節
PF_INET6 AF_INET TCP/IPv6協議族,26字節

故地址族的填寫由當前使用的協議族決定。

可以看到sa_data[14]無法完全存儲各種協議族的地址。因此Linux定義了下面這個新的socket地址結構體:struct sockaddr_storage

# include <bits/socket.h>
struct sockaddr_storage
{
     sa_family_t sa_family;//地址族類型變量
     unsigned long int_ss_align;//內存對齊
     char _ss_padding[128-sizeof(_ss_align)];
};

這個結構體提供了足夠大的空間存放地址值。這兩個結構體是通用的socket地址結構體。

(二)專用socket地址

通用結構體很不好用,比如設置與獲取IP地址和端口號就需要執行繁瑣的操作,所以Linux爲各個協議提供了專門的socket地址結構體

【1. UNIX本地域協議族PF_UNIX:】 使用sockaddr_un地址結構體

# include <sys/un.h>
struct sockaddr_un
{
     sa_family_t sa_family;//地址族類型變量
     char sun_path[108];//文件路徑名
};

【2. TCP/IP協議族PF_INET:】sockaddr_insockaddr_in6兩個專用socket地址結構體,分別用於IPv4IPv6,我們現在使用的地址一般爲IPv4,所以我們對sockaddr_in結構體說明,IPv6的感興趣的可以自己去看。

struct sockaddr_in
{
   sa_family_t sin_family;//地址族:AF_INET
   u_int16_t sin_port;//端口號,要用網絡字節序表示(下面詳細講解)
   struct in_addr sin_addr;//IPv4地址結構體,見下面
};
struct in_addr
{
    u_int32_t s_addr;//IPv4地址,要用網絡字節序表示
};

所有專用socket地址類型的變量在實際使用時都需要轉化爲通用socket地址類型sockaddr(強制轉換即可),因爲所有協議族底層的socket編程接口使用的都是 struct_sockaddr 結構體參數。即:
在這裏插入圖片描述
所以要記得強轉,不然會出錯。

(三)主機字節序(小端)和網絡字節序(大端)

【1. 大、小端概念:】

字節序問題:現代CPU的累加器一次都能裝載至少4字節,這裏考慮32位機,即一個整數。那麼這4字節在內存中排列的順序將影響它被累加器裝載成的整數的值。

字節序又分爲大端字節序小端字節序

  • 大端字節序:指一個整數的高位字節(23~31)存儲在內存的低地址處低位字節(0~7bit)存儲在內存的高地址處大端字節序也稱爲網絡字節序

  • 小端字節序:指整數的高位字節存儲在內存的高地址處,而低位字節則存儲在內存的低地址處。 現代PC大多采用小端字節序,因此小端字節序稱爲主機字節序

在這裏插入圖片描述
使用大端:手機,虛擬機;使用小端:inter芯片

【2. 大、小端轉換的原因:】

當格式化的數據在兩臺使用不同字節序的主機之間直接傳遞時,接收端必然錯誤的解釋之。解決辦法是:

  • 發送端總是把要發送的數據轉化爲大端字節序數據後再發送。
  • 接收端知道對方傳送過來的數據總是採用大端字節序,所以接收端可以根據自身採用的字節序決定是否對接收到的數據進行轉換(小端機轉換,大端機不轉換)。

大端字節序也稱爲網絡字節序它給所有接收數據的主機提供了一個正確解釋收到的格式化數據的保證

要注意的是,即使是同一臺機器上的兩個進程,比如一個用C語言編寫,一個用JAVA編寫通信,也需要考慮大端字節序的問題。

【3.大(網絡字節序)、小(主機字節序)端的轉換函數】

# include<arpa/inet.h>
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);

htonl就是“host to network long"即將long類型的主機字節序轉換爲網絡字節序。其他的解釋一樣,就是類型不一樣,字母順序不一樣。

  • htonl函數一般用來轉換IP地址。
  • htons函數一般用來轉換端口號(在TCP編程sockaddr_in結構體的成員port端口號就需要這個函數轉換

所以任何格式化的數據通過網絡傳輸時,都應該使用這些函數來轉換字節序。

(四)IP地址轉換函數

通常,人們使用可讀性好的字符串來表示IP地址,比如用點分十進制字符串表示IPv4地址,以及用十六進制字符串表示IPv6地址。但是編程時我們需要把它們轉換爲二進制纔可以使用,但是在記錄日誌時則相反,我們需要把整數表示的IP地址轉化爲可讀的字符串

那麼就必須要將用點分十進制字符串表示的IPv4地址用網絡字節序(大端)整數表示的IPv4地址進行轉換,系統提供了三個函數

# include<arpa/inet.h>
in_addr_t inet_addr(const char* strptr);
int inet_aton(const char* cp,struct in_addr* inp);
char* inet_ntoa(struct in_addr in);
  • inet_addr函數用點分十進制字符串表示的IPv4地址轉化爲用網絡字節序整數表示的IPv4地址,失敗時返回INADDR_NONE。一般在TCP編程sockaddr_in結構體成員sin_addr即IPv4地址需要用到這個函數)
  • inet_aton函數完成和inet_addr同樣的功能,但是將轉化結果存儲於參數inp指向的地址結構中,成功返回1,失敗返回0。
  • inet_ntoa函數將用網絡字節序整數表示的IPv4地址轉化爲用點分十進制字符串表示的IPv4地址。

(五)端口號

我們說過,在網絡中通訊的主角是運行在不同主機上的兩個進程。可以通過IP地址標識主機,端口號標識進程。
我們看一下端口號的分類:

範圍 含義
0~1023 被公認的服務佔用了,用戶不能使用,如Web服務佔用80端口
1024~49151 用戶自己使用的端口號,即自己定義TCP編程sockaddr_in結構體的成員port端口號
49152~65535 用於自動分配,如我們電腦安裝的客戶端程序

那我們常見的知名端口號有:

公認服務 端口號
FTP文件傳輸服務 21
SSH 22
Telnet終端仿真服務 23
SMTP簡單郵件傳輸服務 25
DNS 域名解析服務
HTTP超文本傳輸服務 80
HTTPS加密的超文本傳輸服務 443
POP3郵局協議版本3 110
騰訊QQ 8000

在Linux下使用cat /etc/serveices可查看知名的端口號。

二、TCP概述

TCP(Transmission Control Protocol 傳輸控制協議)是一種面向連接的、可靠的、基於字節流的傳輸層通信協議。

【1. TCP的特點】:

  • 面向連接的傳輸協議:每一次完整的數據傳輸都要經過建立連接、使用連接、終止連接的過程;
  • 可靠、出錯重傳、且每收到一個數據都要給出相應的確認,保證數據傳輸的可靠性;
  • TCP連接是基於字節流的,而非報文;傳輸單位爲數據段,每次發送的TCP數據段大小和數據段數都是可變的;
  • 僅支持單播傳輸,支持全雙工傳輸。

【2. TCP的優缺點】:

優點:

可靠,穩定。主要體現在:

  • TCP在數據傳遞之前,會有三次握手來建立連接連接;
  • 在數據傳遞時,採用校驗和,序列號,確認應答,超時重發,流量控制,滑動窗口等機制保證了可靠,提高了性能。
  • 在數據傳送完後,會斷開連接以節約資源。

缺點:

  • 傳輸速度慢;因爲在TCP傳送數據前,要建立連接,耗費時間,數據傳遞中又適用了很多機制來保證其可靠,也會消耗大量的時間。
  • 效率低,佔用系統資源多;它要維護所有所有傳輸連接,每個連接都會佔用系統的CPU,內存等資源。
  • 易被攻擊;因爲其本身的機制,在三次握手確認連接時,容易受到DOS、STN洪泛攻擊等。

【3. TCP適用場景】:

TCP 適用於對可靠性、數據的傳輸質量要求高,但對實時性要求不高的場景,如 HTTP、HTTPS、FTP 等傳輸文件的協議以及 POP、SMTP 等郵件傳輸的協議。

【4. 運行於 TCP 協議之上的協議】:

  • HTTP 協議:超文本傳輸協議,用於普通瀏覽
  • HTTPS 協議:安全超文本傳輸協議,身披 SSL 外衣的 HTTP 協議
  • FTP 協議:文件傳輸協議,用於文件傳輸
  • POP3 協議:郵局協議,收郵件使用
  • SMTP 協議:簡單郵件傳輸協議,用來發送電子郵件
  • Telent 協議:遠程登陸協議,通過一個終端登陸到網絡
  • SSH 協議:安全外殼協議,用於加密安全登陸,替代安全性差的 Telent 協議

三、TCP網絡編程函數

下面的函數都是Linux系統調用函數,我們需要學習如何使用系統調用。

Linux上一切皆文件,所以socket套接字也是文件,分類在設備文件中,文件標識符爲s。所以socket就是一個可讀,可寫,可控制,可關閉的文件描述符。

(一)socket()創建

首先創建socket套接字,我們使用socket系統調用創建一個socket,函數原型爲:

# include<sys/types.h>
# include<sys/socket.h>
int socket(int domin,int type,int protocol);
             //調用成功返回一個socket文件描述符,失敗返回-1,並設置errno

參數:

  • domain參數告訴系統使用哪個底層協議族,我們在上面列出了協議族和對應的地址族。如果爲 TCP/IP協議,參數設置爲PF_INET(用於IPv4)或PF_INET6(用於IPv6);對於UNIX協議族,設置爲PF_UNIX。
  • type參數指定服務器類型。服務器類型主要有:
    (1)SOCK_STREAM流服務,用於TCP。
    (2)SOCK_DGRAM數據報服務,用於UDP。
  • protocol參數是在前兩個參數構成的協議集合下,再選擇一個具體的協議一般都設置爲0,標識默認協議

創建socket時,指定了它的地址族,但是並未指定使用該地址族中的哪個具體socket地址,所以需要下一步命名綁定。

(二)bind()命名綁定

將一個socket與socket地址綁定稱爲給socket命名。在服務器程序中,我們通常要命名綁定socket,因爲只有命名後客戶端才能知道該如何連接它,而客戶端通常不需要命名socket,會採取匿名方式,即使用操作系統自動分配的socket地址。

命名socket的系統調用是bind,函數原型爲:

# include<sys/types.h>
# include<sys/socket.h>
int bind(int sockfd,const struct sockaddr* my_addr,socklen_t addrlen);
           //成功返回0,失敗返回-1,並設置errno

參數:

  • sockfd:爲socket文件描述符,socket系統調用函數的返回值。
  • my_addr:表示將所指的socket地址分配給未命名的sockfd文件描述符,我們上面說過通用地址使用不方便,所以在這我們使用TCP/IP專用socket地址即sockaddr_in。使用前先定義結構體,再將參數傳入,參數包含地址族,端口號,IPv4地址,需要用到上面說的函數進行一定的轉換,具體如下:
    struct sockaddr_in ser_addr; //定義結構體
    memset(&ser_addr,0,sizeof(ser_addr));//清空結構體,全爲0
    ser_addr.sin_family=AF_INET;//設置地址族,因爲當前所用爲TCP/IPv4,對應的地址族爲AF_INET
    ser_addr.sin_port=htons(6000);//端口號,需要將short類型的主機字節序轉化爲網絡字節序
    ser_addr.sin_addr.s_addr=inet_addr("127.0.0.1");//將點分十進制IPv4轉換爲網絡字節序整數標的IPv4地址,此地址爲迴環測試地址,也可以輸入本機IP
    
    在bind中使用時,需要強制轉換爲sockaddr類型的,即:
    (struct  sockaddr*)&ser_addr
    
    因爲socket編程底層都是sockaddr結構體。
  • addrlen參數指出該socket地址的長度。用sizeof即可得知。

那麼bind的使用就是:

int res=bind(sockfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));

常見bind失敗返回-1並設置errno的值和原因爲:

  • EACCES:被綁定的地址是受保護的地址,僅超級用戶纔可以,如將socket綁定到知名服務端口(0~1023)上時,就會返回這個。
  • EADDRINUSE:被綁定的地址正在使用中。

(三)listen()啓動監聽

socket命名後,還不能馬上接受客戶連接,我們需要使用如下系統調用來創建一個監聽隊列以存放待處理的客戶連接:

# include<sys/socket.h>
int listen(int sockfd,int backlog);
              //成功返回0,失敗返回-1,並設置errno

注意:listen不會阻塞,因爲它只是啓動監聽

參數

  • sockfd參數: 指定被監聽的socket,socket系統調用函數的返回值。
  • backlog參數: 表示內核監聽隊列的最大長度,監聽隊列的長度如果超過backLog,服務器將不受理新的客戶連接,客戶端也將收到ECONNREFUSED錯誤信息。backlog參數的典型值是5

內核版本2.2之前的Linux中,backlog參數是指所有處於半連接狀態(未完成三次握手的SYN_RCVD),和完全連接狀態(完成三次握手的ESTABLISHED)的socket的上限,即如下圖:
在這裏插入圖片描述
但自內核版本2.2之後,它只表示處於完全連接狀態的socket的上限,處於半連接狀態的socket的上限由/pro/sys/net/ipv4/tcp_max_syn_backlog內核參數定義。

我們可以使用 命令netstat

netstat //查看服務器上連接的狀態

監聽隊列中完整連接的上限通常比backlog值略大,可能多1個,也可能多兩個,Linux上完整連接最多爲(backlog+1)個,即如果backlog的值爲5,那麼在監聽隊列中,處於ESTABLISHED完全連接狀態的連接有6個,如果還有其他連接,當6個滿了後,它們都會處於SYN_RCVD半連接狀態。

服務端通過listen調用來被動接受連接,我們把執行過listen調用處於LISTEN狀態的套接字稱爲監聽socket;所有處於ESTABLISHED狀態的socket則稱爲連接socket。

(四)connect()建立連接

服務器通過listen調用被動接受連接,那麼客戶端需要通過如下系統調用來主動與服務器建立連接

# include<sys/types.h>
# include<sys/socket.h>
int connect(int sockfd,const struct sockaddr* serv_addr,socklen_t addrlen);
             //成功返回0,一旦成功建立連接,sockfd就唯一標識了這個連接,客戶端就可以通過讀寫sockfd來與服務器通信
             //失敗返回-1,設置errno

參數:

  • sockfd: 由socket系統調用返回的sockfd。
  • serv_addr: 是服務器監聽的socket地址。注意定義socketaddr_in結構體存儲的是服務器的socket信息,所以應該和服務器的初始化一樣,而不是客戶端的。
  • addrlen: 指定這個地址的長度。

所以一般使用爲:

struct sockaddr_in ser;//定義socket地址,存儲服務器socket地址
memset(&ser,0,sizeof(ser));
ser.sin_family=AF_INET;//地址族
ser.sin_port=htons(6000);//端口號
ser.sin_addr.s_addr=inet_addr("127.0.0.1");//IP地址
int res=connect(sockfd,(strcut sockaddr*)&ser,sizeof(ser));

connect的執行在listen之後,accept之前

(五)accept()接受連接

從listen監聽隊列中接受一個連接。函數原型爲:

# include<sys/types.h>
# include<sys/socket.h>
int accept(int sockfd,struct sockaddr* addr,socklen_t* addrlen);
       //成功時返回一個新的連接socket,該socket唯一的標識了被接受的這個連接,服務器可以通過讀寫該socket來與被接受連接對應客戶端通信;
       //失敗返回-1,設置errno

參數:

  • sockfd:執行過listen系統調用的監聽socket。即socket系統調用返回的sockfd經過listen系統調用
  • addr參數:用來獲取被接受連接的遠端socket地址(即客戶端),所以我們需要再定義一個socketaddr_in結構體來存儲客戶端的socket地址,在使用參數時,記得強轉。
  • addrlen參數:爲socket地址的長度,用sizeof即可。

一般使用爲:

struct sockaddr_in cli_addr;//定義保存客戶端socket地址的結構體
socklen_t len=sizeof(cli_addr);//得到長度
int clifd=accept(listenfd,(struct sockaddr*)&cli_addr,&len);//傳入,執行成功後,cli_addr保存客戶端的socket地址信息

accept只是從監聽隊列中取出連接,而不論連接處於何種狀態(如ESYABLOSHED狀態和CLOSE_WAIT狀態),更不關心任何網絡狀況的變化,就算客戶端斷網了,它還是會正常返回。

(六)recv()讀取、send()發送數據

對文件的讀寫操作read和write同樣適用於socket,到那時socket編程接口提供了幾個專門用於socket數據讀寫的系統調用,它們增加了對數據讀寫的控制,其中用於TCP流數據讀寫的系統調用是:

# include<sys/types.h>
# include<sys/socket.h>
ssize_t recv(int sockfd,void* buf,size_t len,int flags);//讀取數據
ssize_t send(int sockfd,const void*buf,size_t len,int flags);//發送數據
            //成功返回實際讀取/寫入的數據長度,出錯返回-1,設置errno

參數

  • sockfd爲accept函數返回的sockfd,表示被接受的客戶端,可以通過socfkd對他進行讀寫,注意不是socket函數返回的sockfd。
  • buf:緩衝區位置
  • len:緩衝區大小
  • flags:爲數據收發提供了額外的控制,一般設置爲0。

recv函數成功返回 實際讀取到的數據的長度,它可能小於我們期望的長度len,因此我們可能要多次調用recv,才能讀取到完成的數據。recv可能返回0,這意味着通信對方已經關閉連接,沒有讀到數據。send函數往sockfd表示的客戶端寫入數據,如下圖所示:
在這裏插入圖片描述

(七)close()關閉連接

關閉連接實際上就是關閉連接對應的socket,和關閉普通文件描述符方法一樣,系統調用如下:

# include<unistd.h>
int close(int fd);
    //成功返回0,失敗返回-1,設置errno

fd參數: 是待關閉的socket。

但是close系統調用並非總是立即關閉一個連接,而是將fd的引用計數減1只有當fd的引用技術爲0時,才能真正的關閉。這個概念我們在父子進程共享文件時也說過,即一次fork系統調用默認將使父進程中打開的socket的引用計數加一,所以必須在父、子進程中都對該socket執行close調用才能將連接關閉。

close這種關閉方法是專門爲了網絡編程設計的,如果要立即終止連接,而不是將socket的引用計數減一,可以使用shutdown調用,它可以關閉讀、寫或全部關閉。

四、TCP網絡編程流程

(一)編程流程

現在我們需要將進程分爲:服務器(主動),客戶端(被動)兩種類型,其中:

  • 服務器和多個客戶器連接,所以服務器複雜。
  • 客戶器和一個服務器連接。所以客戶器簡單。

我們可以根據TCP編程函數寫出服務器和客戶端編程所用的函數流程:

服務器:

int socket();創建一個用於監聽客戶端連接的網絡套接字《----》買手機
int bind();將創建的套接字與本端的地址信息進行綁定IP+端口《----》給手機插卡,綁定電話號碼,不然別人無法撥號聯繫我
int listen();啓動監聽,不會阻塞《----》開機,不用一直等着別人給你打電話
int accept():接受一個客戶端的連接,返回的是一個客戶端連接套接字《----》如果有人打電話,我就接聽電話
int recv()/send();讀取數據或者發送數據《----》交談
int close();關閉文件描述符《----》掛電話

客戶端:

int socket();創建一個用於整個通訊的套接字《----》買手機
int connect();與服務器程序建立連接《----》撥號
int recv()/send();讀取或發送數據《----》交談
int close();關閉連接《----》掛電話

但是這樣的流程會出現很多問題,如下:

  • 電話接聽後,兩個人同時說話,就會無法交談。即必須規定客戶端和服務器發送數據的順序。
  • 不能說一句話掛一次電話,再撥號再說下一句。所以循環多次進行數據交互,故recv/send必須在while循環內,說完後我再掛斷電話
  • 不能接聽一個人的電話就關機一次,也不能只接聽一個人的電話。所以服務器要一直開啓,循環接受處理客戶端連接,故accept在while循環內,直到全部結束,關閉監聽套接字。

所以對整個流程進行一個改進,用僞代碼進行一個描述:
在這裏插入圖片描述

(二)編程模型圖

我們將整個編程流程畫圖展示:
在這裏插入圖片描述

(三)編程實例

實現一個簡單的TCP通信,客戶端和服務器建立連接,發送數據,服務器收到數據,回覆信息。
【Tcpcli.c】

# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<assert.h>
# include<string.h>
# include<sys/types.h>
# include<sys/socket.h>
# include<netinet/in.h>
# include<arpa/inet.h>


int main()
{
    int listenfd=socket(AF_INET,SOCK_STREAM,0);//創建套接字
    assert(listenfd!=-1);

    struct sockaddr_in ser_addr;//定義服務器TCP/IP專用socket地址
    memset(&ser_addr,0,sizeof(ser_addr));//置爲空,防止下一個客戶端無法連接
    ser_addr.sin_family=AF_INET;//設置地址族
    ser_addr.sin_port=htons(6000);//設置端口號,將主機字節序轉換爲網絡字節序
    ser_addr.sin_addr.s_addr=inet_addr("127.0.0.1");//設置IPv4,將點分十進制轉換爲網絡字節序整數表示的IPV4地址
    int res=bind(listenfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));//命名綁定socket
    assert(res!=-1);

    res=listen(listenfd,5);//監聽,監聽隊列爲5
    assert(res!=-1);

    //循環接受客戶端連接,
    while(1)
    {
        struct sockaddr_in cli_addr;//定義客戶端socket地址
        socklen_t len=sizeof(cli_addr);

        int clientfd=accept(listenfd,(struct sockaddr*)&cli_addr,&len);//接收一個客戶端
        if(clientfd==-1)
        {
            printf("one client link error\n");
            continue;
        }
        printf("one client success--%s:%d\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port));
        //循環接受,發送數據
        while(1)
        {
            char buff[128]={0};
            int num=recv(clientfd,buff,127,0);//接收數據
            if(num==-1)//接收失敗
            {
                printf("recv error\n");
                break;
            }
            else if(num==0)//客戶端關閉
            {
                printf("client over\n");
                break;
            }
            printf("recv data is:%s\n",buff);

            char* restr="recv data success";
            num=send(clientfd,restr,strlen(restr),0);//回覆數據
            if(num==-1)
            {
                printf("send data error\n");
                break;
            }
        }
        close(clientfd);//關閉客戶端連接,服務器還可以接收下一個客戶端
    }
    close(listenfd);//關閉服務器
    exit(0);
}


【Tcpcli.c】

# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<assert.h>
# include<sys/types.h>
# include<sys/socket.h>
# include<netinet/in.h>
# include<arpa/inet.h>
# include<string.h>

int main()
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);//創建套接字
    assert(sockfd!=-1);

    //客戶端必須定義服務器的,否則無法連接
    struct sockaddr_in ser_addr;//定義服務器TCP/IP專用socket地址
    memset(&ser_addr,0,sizeof(ser_addr));//置爲空,防止下一個客戶端無法連接
    ser_addr.sin_family=AF_INET;//設置地址族
    ser_addr.sin_port=htons(6000);//設置端口號,將主機字節序轉換爲網絡字節序
    ser_addr.sin_addr.s_addr=inet_addr("127.0.0.1");//設置IPv4,將點分十進制轉換爲網絡字節序整數表示的IPV4地址
    
    int res=connect(sockfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));//連接服務器
    assert(res!=-1);
    //循環接受客戶端連接,
    while(1)
    {
        printf("please input:");
        char data[128]={0};
        fgets(data,127,stdin);
        if(strncmp(data,"bye",3)==0)
        {
            break;
        }
        int num=send(sockfd,data,strlen(data)-1,0);
        assert(num!=-1);
        if(num==0)
        {
            printf("send length is zero\n");
            break;
        }
        char buff[128]={0};
        int n=recv(sockfd,buff,127,0);
        assert(n!=-1);
        if(n==0)
        {
            printf("error\n");
            break;
        }
        printf("recv ser data is:%s\n",buff);
    }
    close(sockfd);//關閉服務器
    exit(0);
}

運行一個客戶端和服務器連接,正常通信。

在這裏插入圖片描述
再運行一個會發現,沒有和服務端連接,發送的數據也不能發過去,這是因爲目前我們的服務器在一個時間只能連接一個客戶端,所以第二個客戶端不能被連接。

在這裏插入圖片描述

當我們關掉第一個客戶端,可以看到第二個客戶端成功連接,發出的信息也成功被收到。
在這裏插入圖片描述

加油哦!💪。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章