TCP模型創建流程圖
TCP套接字編程中的接口
socket 函數
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:
- AF_INET 這是大多數用來產生socket的協議,使用TCP或UDP來傳輸,用IPv4的地址
- AF_INET6 與上面類似,不過是來用IPv6的地址
- AF_UNIX 本地協議,使用在Unix和Linux系統上,一般都是當客戶端和服務器在同一臺及其上的時候使用
type:
- SOCK_STREAM 這個協議是按照順序的、可靠的、 數據完整的基于字節流的連接。 這是一個使用最多的socket類型,這個socket 是使用TCP來進行傳輸。
- SOCK_DGRAM 這個協議是無連接的、固定長度的傳輸調用。該協議是不可靠的,使用UDP來進行它的連接。
- SOCK_SEQPACKET該協議是雙線路的、可靠的連接,發送固定長度的數據包進行傳輸。必須把這個包完整的接受才能進行讀取。
- SOCK_RAW socket類型提供單一的網絡訪問,這個socket類型使用ICMP公共協議。 (ping、traceroute使用該協議)
- SOCK_RDM 這個類型是很少使用的,在大部分的操作系統上沒有實現,它是提供給數據鏈路層使用,不保證數據包的順序
protocol:
傳0 表示使用默認協議。
返回值
成功:返回指向新創建的socket的文件描述符,失敗:返回-1,設置errno
socket函數的作用
socket()打開一個網絡通訊端口,如果成功的話,就像 open()一樣返回一個文件描述符,應用程序可以像讀寫文件一樣用 read/write 在網絡上收發數據,如果 socket()調用出錯則返回-1。對于 IPv4,domain 參數指定為 AF_INET。 對于 TCP 協議,type 參數指定為 SOCK_STREAM,表示面向流的傳輸協議。如果是 UDP 協議,則 type 參數指定為 SOCK_DGRAM,表示面向數據報的傳輸協議。protocol 參數的介紹從略,指定為 0 即可。
bind 函數
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:
socket文件描述符
addr:
構造出IP地址加端口號
addrlen:
sizeof(addr)長度 返回值: 成功返回0,失敗返回-1, 設置errno
bind函數的作用
-
服務器程序所監聽的網絡地址和端口號通常是固定不變的,客戶端程序得知服務器程序的地址和端口號后就可 以向服務器發起連接,因此服務器需要調用 bind 綁定一個固定的網絡地址和端口號。
-
**bind()的作用是將參數 sockfd 和 addr 綁定在一起,使 sockfd 這個用于網絡通訊的文件描述符監聽 addr 所描述 的地址和端口號。**前面講過,
structsockaddr*
是一個通用指針類型,addr 參數實際上可以接受多種協議的 sockaddr 結構體,而它們的長度各不相同,所以需要第三個參數 addrlen 指定結構體的長度。如:struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(6666); -
首先將整個結構體清零,然后設置地址類型為
AF_INET
,網絡地址為INADDR_ANY
,這個宏表示本地的任意 IP 地址,因為服務器可能有多個網卡,每個網卡也可能綁定多個 IP 地址,這樣設置可以在所有的 IP 地址上監聽,直 到與某個客戶端建立了連接時才確定下來到底用哪個 IP 地址,端口號為 6666。
accept 函數
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockdf:
socket文件描述符
addr:
傳出參數,返回鏈接客戶端地址信息,含IP地址和端口號
addrlen:
傳入傳出參數(值-結果),傳入sizeof(addr)大小,函數返回時返回真正接收到地址結構體的大小
返回值:
成功返回一個新的socket文件描述符,用于和客戶端通信,失敗返回-1,設置errno
accpet函數的作用
三方握手完成后,服務器調用 accept()接受連接,如果服務器調用 accept()時還沒有客戶端的連接請求,就阻塞 等待直到有客戶端連接上來。addr 是一個傳出參數,accept()返回時傳出客戶端的地址和端口號。addrlen 參數是一 個傳入傳出參數(value-resultargument),傳入的是調用者提供的緩沖區 addr 的長度以避免緩沖區溢出問題,傳出 的是客戶端地址結構體的實際長度(有可能沒有占滿調用者提供的緩沖區) 。如果給 addr 參數傳 NULL,表示不關心 客戶端的地址。
示例
while (1) { cliaddr_len = sizeof(cliaddr); connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); n = read(connfd, buf, MAXLINE); ......close(connfd);}
整個是一個 while 死循環,每次循環處理一個客戶端連接。由于 cliaddr_len 是傳入傳出參數,每次調用 accept() 之前應該重新賦初值。accept()的參數 listenfd是先前的監聽文件描述符,而 accept()的返回值是另外一個文件描述符 connfd,之后與客戶端之間就通過這個 connfd 通訊,最后關閉 connfd斷開連接,而不關閉 listenfd,再次回到循環 開頭 listenfd 仍然用作 accept 的參數。accept()成功返回一個文件描述符,出錯返回-1。
connect 函數
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockdf:
socket文件描述符
addr:
傳入參數,指定服務器端地址信息,含IP地址和端口號
addrlen:
傳入參數,傳入sizeof(addr)大小
返回值:
成功返回0,失敗返回-1,設置errno
客戶端需要調用 connect()連接服務器,connect 和 bind 的參數形式一致,區別在于 bind 的參數是自己的地址, 而 connect 的參數是對方的地址。connect()成功返回 0,出錯返回-1。
C/S 模型-TCP
Tcp通信的實現
封裝接口
tcpsocket.hpp
/* * 封裝Tcpsocket類,向外提供更加輕便的tcp套接字接口* 1.創建套接字 Socket()* 2.綁定地址信息 Bind(std::string &ip,uint16_t port)* 3.服務端開始監聽 Listen(int backlog = 5)* 4.服務端獲取已完成連接的客戶端socket Accept(TcpSocket &clisock,std::string &cli_ip,uint16_t port)* 5.接受數據 Rec(std::string &buf)* 6.發送數據 Send(std::string &buf)* 7.關閉套接字 Close()* 8.客戶端向服務器發起連接請求 Connect(std::string &srv_ip,uint16_t srv_port)*/#include<iostream>
#include<string>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>#define CHECK_RET(q) if((q) == false){return -1;} class TcpSocket
{public:TcpSocket(){} ~TcpSocket(){} bool Socket(){_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if(_sockfd < 0){ perror("socket error");return false;} return true;} bool Bind(std::string &ip,uint16_t port){struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = inet_addr(ip.c_str());socklen_t len = sizeof(struct sockaddr_in);int ret = bind(_sockfd, (struct sockaddr *)&addr ,len);if(ret < 0){perror("bind error");return false;}return true;}bool Listen(int backlog = 5){//int listen (int sockfd,int backlog)//sockfd: 套接字描述符//backlog:最大并發連接數--決定內核中已完成連接隊列結點個數//backlog決定的不是服務端能接受的客戶端最大上限int ret =listen(_sockfd,backlog);if(ret < 0){perror("listen error");return false;}return true;}bool Accept(TcpSocket &cli,std::string &cliip,uint16_t &port){//int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);//sockfd:套接字描述符//addr: 新建連接的客戶端地址信息//addrlen: 新建客戶端的地址信息長度//返回值:返回新建客戶端socket的描述符struct sockaddr_in addr;socklen_t len = sizeof(struct sockaddr_in);int sockfd = accept(_sockfd,(sockaddr*)&addr,&len);if(sockfd < 0){perror("accept error");return false;}cli.SetFd(sockfd);cliip = inet_ntoa(addr.sin_addr);port = ntohs(addr.sin_port); return true; }void SetFd(int sockfd){_sockfd = sockfd;}bool Connect(std::string &srv_ip,uint16_t srv_port){//int connect(int sockfd,const struct sockaddr *adrr,socklen_t addrlen);//sockfd :套接字描述符//addr:服務端地址信息//addrlen:地址信息長度struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(srv_port);addr.sin_addr.s_addr = inet_addr(srv_ip.c_str());socklen_t len = sizeof(struct sockaddr_in);int ret = connect(_sockfd,(struct sockaddr*)&addr,len);if(ret < 0){perror("connect error");return false;}return true;}bool Recv(std::string &buf){//ssize_t recv(int sockfd,void *buf ,size_t len,int flags)//sockfd :服務端為新客戶端新建的socket描述符//flags: 0--默認阻塞接受 沒有數據一直等待// MSG_PEEK 接受數據,但是數據并不從緩沖區移除// 常用于探測性數據接受//返回值:實際接收字節長度;出錯返回-1;若連接斷開則返回0//recv默認阻塞的,意味著沒有數據則一直等,不會返回0//返回0只有一種情況,就是連接斷開,不能再繼續通信了char tmp[4096];int ret = recv(_sockfd,tmp,4096,0);if(ret < 0){perror("recv error");return false;}else if(ret == 0){printf("peer shutdown\n");return false;}buf.assign(tmp,ret);return true;}bool Send(std::string &buf){//ssize_t send (int sockfd,void *buf,size_t len,int flags)int ret = send(_sockfd,buf.c_str(),buf.size(),0);if(ret < 0){perror("send error");return false;}return true;}bool Close(){close(_sockfd);return true;}private:int _sockfd;
};
客戶端實現
/**基于封裝的Tcpsocket實現tco客戶端程序1.創建套接字2.為套接字綁定地址信息(不推薦用戶手動綁定)while(1){
4.發送數據
5.接收數據}6.關閉套接字*/#include"tcpsocket.hpp"int main(int argc,char *argv[]){if(argc != 3){ std::cout<<"./tco_cli 192.168.145.132 9000\n";return -1; } std::string ip = argv[1];uint16_t port = atoi(argv[2]);TcpSocket sock;CHECK_RET(sock.Socket());CHECK_RET(sock.Connect(ip,port));while(1){std::string buf;std::cout<<"client say:";fflush(stdout);std::cin >> buf;sock.Send(buf);buf.clear();sock.Recv(buf);std::cout<<"server say:"<<buf<<std::endl; } sock.Close();return 0;
}
服務端的實現
/**基于封裝的tcpsocket,實現tcp服務端程序1. 創建套接字2.為套接字綁定地址信息3.開始監聽,如果有連接進來,自動完成三次握手while(1){4,從已完成連接隊列,獲取新建的客戶端連接socket5.通過新建的客戶端連接socket,與指定的客戶端進行通信,recv6.send}7. 關閉套接字*/#include"tcpsocket.hpp"int main(int argc,char *argv[]){if(argc != 3){ std::cout<<"./tcp_srv 192.168.145.132 9000\n";return -1; } std::string ip =argv[1];uint16_t port =atoi(argv[2]);TcpSocket sock;CHECK_RET(sock.Socket());CHECK_RET(sock.Bind(ip,port));CHECK_RET(sock.Listen());while(1){TcpSocket clisock;std::string cliip;uint16_t cliport; if(sock.Accept(clisock,cliip,cliport) == false){ //當已完成的連接隊列中沒有socket,會阻塞 continue;} std::cout<<"new client:"<<cliip<<":"<<cliport<<std::endl;std::string buf;clisock.Recv(buf);std::cout<<"client say:"<<buf<<std::endl;buf.clear();std::cout<<"server say:";fflush(stdout);std::cin>>buf;clisock.Send(buf);}sock.Close();
}
程序出現的問題
這個實現的最基本的tcp服務端程序中,因為服務端不知道客戶端數據什么時候到來,因此程序只能寫死;但是寫死就有可能會造成阻塞(accep/recv),導致服務端無法同時處理多個客戶端的請求
多進程tcp服務端程/多線程版本tcp服務端程序
適用多進程tcp服務端程序的處理多客戶端請求;每當一個客戶端的連接到來,都創建一個新的子進程,讓子進程單獨與客戶端進行通信;這樣的話父進程永遠只處理新連接
TCP套接字多進程版本
#include<signal.h>
#include"tcpsocket.hpp"
#include<sys/wait.h>void sigcb(int no){while(waitpid(-1,NULL,WNOHANG) > 0);
}int main(int argc,char *argv[]){if(argc != 3){std::cout<<"./tcp_srv 192.168.145.132 9000\n";return -1;}std::string ip =argv[1];uint16_t port =atoi(argv[2]);signal(SIGCHLD,sigcb);TcpSocket sock;CHECK_RET(sock.Socket());CHECK_RET(sock.Bind(ip,port));CHECK_RET(sock.Listen());while(1){TcpSocket clisock;std::string cliip;uint16_t cliport; if(sock.Accept(clisock,cliip,cliport) == false){ //當已完成的連接隊列中沒有socket,會阻塞continue;}std::cout<<"new client:"<<cliip<<":"<<cliport<<std::endl;int pid =fork();if(pid == 0){//子進程專門處理每個客戶端的通信while(1){std::string buf;clisock.Recv(buf);std::cout<<"client say:"<<buf<<std::endl;buf.clear();std::cout<<"server say:";fflush(stdout);std::cin>>buf;clisock.Send(buf);}clisock.Close();exit(0);}//父進程關閉套接字,因為父子進程數據獨有//不關閉,會造成資源泄露clisock.Close();//父進程不通信}sock.Close();
}
TCP套接字多線程版本
#include"tcpsocket.hpp"
#include<pthread.h>
void* thr_start(void *arg){TcpSocket *clisock = (TcpSocket *)arg;while(1){//因為線程之間,共享文件描述符表,因此在一個線程中打開的文件//另一個線程只要能夠獲取文件描述符,就能在操作文件std::string buf;clisock->Recv(buf);std::cout<<"client say:"<<buf<<std::endl;buf.clear();std::cout<<"server say:";fflush(stdout);std::cin>>buf;clisock->Send(buf);}clisock->Close();delete clisock;return NULL;
}int main(int argc,char *argv[]){if(argc != 3){std::cout<<"./tcp_srv 192.168.145.132 9000\n";return -1;}std::string ip =argv[1];uint16_t port =atoi(argv[2]);TcpSocket sock;CHECK_RET(sock.Socket());CHECK_RET(sock.Bind(ip,port));CHECK_RET(sock.Listen());while(1){TcpSocket *clisock = new TcpSocket();std::string cliip;uint16_t cliport; if(sock.Accept(*clisock,cliip,cliport) == false){ //當已完成的連接隊列中沒有socket,會阻塞continue;}std::cout<<"new client:"<<cliip<<":"<<cliport<<std::endl;pthread_t tid;pthread_create(&tid,NULL,thr_start,(void *)clisock);pthread_detach(tid);}sock.Close();
}
tcp連接斷開
tcp自己實現了保活機制:當長時間沒有數據通信,服務端會向客戶端發送保活探測包;當這些保活探測包連續多次都沒有響應,則認為連接斷開
recv返回0;send觸發異常