1. 程式人生 > >tcp網路程式設計客戶端和服務端及listen和tcp允許最大連線數

tcp網路程式設計客戶端和服務端及listen和tcp允許最大連線數

tcp網路程式設計
tcp網路程式設計步驟:
由於tcp傳輸特點是可靠有連線,那麼就有
1.客戶端向服務端傳送連線請求(SYN),
2.服務端接受請求並向客戶端傳送(SYN+ACK);
3.客戶端向服務端回覆ACK表明他知道服務端同意連線。
以上三個步驟就是三次握手。
服務端程式設計步驟:
1.建立套接字
2.為套接字繫結地址資訊
3.監聽:開始接受服務端的連線請求
4.獲取連線建立成功的新socket
5.傳送資料
6.接受資料
1.建立套接字

#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:地址域
		 AF_INET :ipv4協議
type: 套接字型別
		SOCK_STREAM 流式套接字
		SOCK_DGRAM  資料報套接字
protocol :協議型別 
		如果是0,則表示預設;流式套接字預設tcp協議,報式套接字預設udp協議
		流式套接字: IPPROTO_TCP 6 
		報式套接字:IPPROTO_UDP 17  
如:socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
返回值:成功:套接字描述符 
	   失敗:-1						

2.為socket繫結地址資訊

 #include <sys/socket.h>
 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
 引數: sockfd: socket描述符
 	   addr :socket繫結的地址
 	   addrlen :地址資訊長度
 返回值:成功:0(網絡卡操作那個程序),失敗 -1
 功能:將引數sockfd和addr繫結在一起,使sockfd這個用於網路通訊的檔案描述符監聽addr所描述的地址和埠號。
 sockaddr結構:
 struct sockaddr {
               sa_f  amily_t sa_family;
               char        sa_data[14];
                }
雖然bind裡引數是sockaddr,但是真正在基於IPV4程式設計時,使用的結構體是sockaddr_in;這個結構體裡主要有三部分資訊:地址型別,埠號,IP地址。
sockaddr_in在標頭檔案#include<netinet/in.h>或#include<arpa/inet.h>中定義。該結構體解決了sockaddr的缺陷,把port和addr 分開儲存在兩個變數中,如下: 
structsockaddr_in{

short           sin_family;//AF_INET(地址族)PF_INET(協議族)

unsigned short  sin_port;/*Portnumber(必須要採用網路資料格式,普通數字可以用htons()函式轉換成網路資料格式的數字)*/

struct in_addr  sin_addr;//32位IP地址

unsigned char   sin_zero[8];//沒有實際意義,只是為了跟SOCKADDR結構在記憶體中對齊*/

};
該結構體中提到的另一個結構體in_addr定義如下,它用來存放32位IP地址:
typedef uint32_t in_addr_t;
struct in_addr
{
	in_addr_t s_addr;
};   
in_addr用來表示一個IPV4的IP地址,其實是一個32位整數。   

客戶端不推薦手動繫結地址資訊 ,因為繫結有可能因為特殊原因失敗,但是客戶端具體使用哪個地址和埠都可以,只要能把資料傳送出去,所以客戶端程式不手動繫結地址,直至傳送資料時,作業系統檢測到socket沒有繫結地址,會自動選擇合適的地址和埠為socket繫結地址,這種資料一般不會出錯。
3.監聽(服務端監聽後才可以接受客戶端連線請求)

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);

listen()宣告sockfd處於監聽狀態,並且最多允許backlog個客戶端處於連線等待狀態,如果接受到更多的連線請求就忽略,一般是5,即代表最大同時併發連線數為5,這個數字並不是tcp最大建立連線數。(文章後面會講述tcp最大建立連線數)
返回值:成功: 0  失敗 -1

4.accept():獲取新建立的socket

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd : socket描述符
addr  :新建立連線的客戶端地址資訊
addrlen :地址資訊長度
返回值:成功:返回新的socket連線描述符
	   失敗:-1
accept是阻塞型函式,如果連線成功的佇列沒有新的連線,將會一直阻塞等待新的客戶端連線

引數sockfd和返回值newsockfd區別:
sockfd :所有連線請求的資料傳送到socket這個緩衝區(包括服務端ip和port),然後進行處理(為這個新建立連線的客戶端新建立一個socket);
newsockfd: 連線建立成功後,連線成功的客戶端傳送的資料都發送到這個新的socket緩衝區(包括服務端ip port和建立連線客戶端ip port)。
5.傳送資料

 #include <sys/types.h>
 #include <sys/socket.h>
 ssize_t send(int sockfd, const void *buf, size_t len, int flags);
 flag :  0預設阻塞傳送資料

由於accept返回的socket描述符中有客戶端ip和port,所以引數中就沒有struct sockaddr_in 和 addrlen,這是和udp傳送資料的區別。同理,tcp和udp接受資料函式引數不同。

6.接受資料

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd: 裡面已經包含從哪兒接受資料資訊,是新的sockfd
buf:用於接受資料
len :用於接受資料長度
flags:  0  預設 阻塞式接收 
返回值 : 錯誤 : -1
        連線關閉 ; 0
        實際接受資料 >0

7.關閉socket描述符

要在任意可能退出的地方關閉對應的socket描述符。
tcp服務端程式碼

// tcp 服務端程式碼
//1.建立套接字
//2.繫結地址資訊
//3.監聽:監聽之後獲取新的socket連線
//4.獲取新的socket連線
//5.接受資料
//6.傳送資料
//7.關閉socket描述符
#include<stdio.h>
#include<errno.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>

int main(int argc,char *argv[]) //將需要繫結的IP地址和port在命令列輸出來
{
        if(argc!=3)
        {   
                printf("Usage:ip and port\n");
        }   
        //1.建立套接字
        int sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(sockfd<0)
        {   
                perror("sockfd error");
                return -1; 
        }   
        //2.繫結地址資訊
        // int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
        struct sockaddr_in ser_addr;
        ser_addr.sin_family=AF_INET;
        ser_addr.sin_addr.s_addr=inet_addr(argv[1]);
        ser_addr.sin_port=(htons)(atoi(argv[2])); 
        int len=sizeof(struct sockaddr_in);
        int ret=bind(sockfd,(struct sockaddr*)&ser_addr,len);
        if(ret<0)
 {
                perror("binf error");
                close(sockfd);
                return -1;
        }

        //3.監聽
        // int listen(int sockfd, int backlog);
        if(listen(sockfd,5)<0)//開始監聽,接受客戶端的連線請求,最大同時併發連線數為5
        {
                perror("listen error");
                close(sockfd);
                return -1;
        }
        //連線建立成功後,服務端會新建立一個socket
        while(1)
        {  //用while迴圈當一個連線斷開後,可以重新獲取新的socket
                //4.獲取新建立的socket
                struct sockaddr_in cli_addr;
                len=sizeof(struct sockaddr_in);
                // int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
                int newsockfd=accept(sockfd,(struct sockaddr*)&cli_addr,&len);//獲取成功,返回新的socket描述符
                if(newsockfd<0)
                {
                        perror("newsockfd error");
                        return -1;
                }
                //連線建立成功:
                printf("new con:%s %d\n",(inet_ntoa)(cli_addr.sin_addr),ntohs(cli_addr.sin_port));
                while(1)  //用迴圈是保證服務端可以和一個客戶端可以多次聊天
                {
                        //5.傳送資料 
                        //tcp協議:獲取新的socket描述符後,新的socket裡包含了服務端和客戶端的地址資訊,所以傳送和接受資料沒有先後之分
                        // ssize_t send(int sockfd, const void *buf, size_t len, int flags);
                        char buff[1024]={0};
                        printf("please send data:");
 scanf("%s",buff);
                        ret=send(newsockfd,buff,strlen(buff),0); //阻塞傳送資料 
                        if(ret<0)
                        {
                                perror("send error");
                                close(newsockfd);
                                return -1;
                        }
                        //6.接受資料
                        //ssize_t recv(int sockfd, void *buf, size_t len, int flags);
                        memset(buff,0x00,1024);
                        len=recv(newsockfd,buff,1023,0);//0預設阻塞接受資料
                        if(len<0)//小於0接受失敗
                        {
                                perror("recv error");
                                close(newsockfd);
                                continue;
                        }
                        else if(len==0)//等於0對端將連線斷開
                        {
                                perror("peer has performed an orderly shutdown");
                                close(newsockfd);
                                continue;
                        }
                        printf("[%s:%d]->%s\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port),buff);
                }
                close(newsockfd);
        }
        close(sockfd);
        return -1;
}

tcp客戶端程式設計步驟
1.建立套接字
2.繫結地址資訊(沒有必要呼叫bind()繫結資訊,否則一臺機器上啟動多個客戶端,就會出現埠號被佔用而導致不能正常連線)
3.向服務端發起連線請求
4.接受資料
5.傳送資料
6.關閉
建立套接字、傳送資料、接受資料即掛壁和服務端一樣。
3.向服務端傳送連線請求

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
addr:要連線的服務端地址
addrlen :地址資訊長度
返回值: 成功; 0  失敗 -1

客戶端程式碼:

//tcp 客戶端程式碼
//1.建立套接字
//2.繫結地址資訊
//3.向服務端傳送連線請求
//4.傳送資料
//5.接受資料
//6.關閉socket描述符

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

int main(int argc,char* argv[])
{
        if(argc!=3)
        {   
                printf("Usage ip and port\n");
        }   
        //1.建立套接字
        int sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(sockfd<0)
        {   
                perror("socket error");
                return -1; 
        }    
        //2.繫結地址資訊(不推薦手動寫繫結資訊程式碼)
        //3.向服務端傳送連線請求
        //int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
        struct sockaddr_in ser_addr;
        ser_addr.sin_family=AF_INET;
        ser_addr.sin_port=(htons)(atoi(argv[2]));  //htons :主機位元組序轉換成網路位元組序
        ser_addr.sin_addr.s_addr=(inet_addr)(argv[1]);//因為argv[]是char*,用atoi使字串轉成整型
        int len=sizeof(struct sockaddr_in);
int ret=connect(sockfd,(struct sockaddr*)&ser_addr,len);
        if(ret<0)
        {
                perror("connect error");
                close(sockfd);
                return -1;
        }
        //連線成功,socket描述符裡有服務端和客戶端IP地址和port
        while(1)
        {
                //4.接受資料
                //ssize_t recv(int sockfd, void *buf, size_t len, int flags);
                char buff[1024]={0};
                ret=recv(sockfd,buff,1023,0);//預設阻塞接受資料
                if(len<0)//小於0接受失敗
                {
                        perror("recv error");
                        close(sockfd);
                        continue;
                }
                else if(len==0)//等於0對端將連線斷開
                {
                        perror("peer has performed an orderly shutdown");
                        close(sockfd);
                        continue;
                }
                // net_ntoa :網路位元組序轉換成點分十進位制IP
                //ntohs  :主機位元組序轉換成網路位元組序
                printf("[%s:%d]say:%s\n",(inet_ntoa)(ser_addr.sin_addr),(ntohs)(ser_addr.sin_port),buff);
                //4.傳送資料
                // ssize_t send(int sockfd, const void *buf, size_t len, int flags);
                memset(buff,0x00,1024);
  printf("please send\n");
                scanf("%s",buff);
                ret=send(sockfd,buff,strlen(buff),0); //預設阻塞接受資料
                if(ret<0)
                {
                        perror("send error");
                        close(sockfd);
                        return -1;
                }
        }
        close(sockfd);
        return 0;
}

客戶端:
在這裡插入圖片描述
服務端:
在這裡插入圖片描述
當客戶端ctrl+c斷開連線後,服務端會提示對端已關閉,這時會有新的客戶端建立連線。
listen引數和tcp最多建立連線數
int listen(int sockfd, int backlog);
backlog:
協議棧使用一個佇列:這個佇列的大小由listen系統呼叫的backlog引數決定。當一個syn包到達後,服務端協議棧回覆syn+ack,然後將這個socket加入這個佇列。當客戶端第三次握手的ack包到達後,再將這個socket的狀態改為ESTABLISHED狀態。這也就意味著這個佇列可以可以容納兩種不同狀態的socket:SYN RECEIVED和 ESTABLISHED,而只有後者可以被accept呼叫返回。當佇列中的連線數(socket)達到backlog個後,系統收到syn將不再回復syn+ack。這種情況下協議棧通常僅僅是將syn包丟掉,而不是回覆rst報文,從而讓客戶端可以重試。
tcp最大連線數:
用ulimit -n 結果是1024,這表示當前使用者的每個程序最多允許同時開啟1024個檔案,這1024個檔案需要除去每個程序必然開啟的標準輸入、標準輸出、標準錯誤、伺服器監聽socket,程序間通訊的unix域socket等檔案,那麼剩下可用於客戶端socket連線的檔案數就只有大概1024-10=1014個,即在預設條件下,基於linux的通訊程式最多允許同時1014個TCP併發連線。