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併發連線。