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

歡迎訪問 生活随笔!

生活随笔

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

编程问答

2-4:套接字(Socket)编程之TCP通信

發布時間:2025/3/15 编程问答 23 豆豆
生活随笔 收集整理的這篇文章主要介紹了 2-4:套接字(Socket)编程之TCP通信 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

  • 一 TCP通信服務端和客戶端——和UDP區別
    • (1)服務端
    • (2)客戶端
  • 二:TCP通信-多進程/線程
    • (1)多進程版本
    • (2)多線程版本
    • (3)線程池版本

一 TCP通信服務端和客戶端——和UDP區別

TCP是面向字節流的,是有連接的,會在服務端和客戶端之間建立一條連接,而UDP顯得就比較簡單,只負責傳遞。在2-3:套接字(Socket)編程之UDP通信這一節詳細敘述了UDP通信及套接字相關內容,本節TCP通信將會在上節的基礎上,對TCP和UDP中代碼的不同部分做以補充。

(1)服務端

tcpServer.h

#include <iostream> #include <string> #include <unistd.h> #include <cstdio> #include <cstring> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> using namespace std;#define BACKLOG 5class tcpServer { private:int _port;int listen_sock; public:tcpServer(int port=8080):_port(port){}void initServer(){listen_sock=socket(AF_INET,SOCK_STREAM,0);//區別UDP,TCP采用流式套接字if(listen_sock < 0){cout<<"套接字創建失敗"<<endl;exit(2);}struct sockaddr_in local;local .sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=htonl(INADDR_ANY);if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0){cout<<"綁定失敗"<<endl;exit(3);}//開啟監聽if(listen(listen_sock,BACKLOG) < 0){cout<<"綁定失敗"<<endl;exit(4);} }//通信服務void Service(int sock){char buffer[1024];while(1){size_t s=recv(sock,buffer,sizeof(buffer)-1,0);//區別UDPif(s > 0){buffer[s]='\0';cout<<"服務端收到客戶端的消息:"<<buffer<<endl;send(sock,buffer,strlen(buffer),0);//區別UDP}if(s==0)//客戶端下線,服務端收到0{cout<<"客戶端已經下線"<<endl;close(sock);//注意關閉套接字,資源是有限的break;}}}void startServer(){sockaddr_in endpoint;while(1){//TCP-acceptsocklen_t len=sizeof(endpoint);int _sock=accept(listen_sock,(struct sockaddr*)&endpoint,&len);if(_sock < 0){cout<<"accept失敗"<<endl;continue;}cout<<"一臺新的客戶端已經連接"<<endl;//拿到套接字進行通信Service(_sock);}}~tcpServer(){close(listen_sock);} };

1:創建套接字

相較于UDP,TCP通信時選擇的套接字是流式套接字

listen_sock=socket(AF_INET,SOCK_STREAM,0);

2:listen監聽
在套接字綁定之前,UDP和TCP基本是一致的。在TCP通信中,有兩套套接字,其中一套用于監聽,稱之為監聽套接字。TCP不同于UDP,如果和客戶端之間沒有連接就不能發送數據,所以要把一個套接字設置為監聽狀態,以便在任意時候客戶端請求服務器時都能有套接字與該客戶端建立連接。關于BACKLOG選項后序再網絡原理里面再做解釋

函數原型如下

#include <sys/socket.h>int listen(int s, int backlog);

3:accept

accept表示服務端接受一個連接,每當一個客戶端連接成功時,就會建立一條連接。最為關鍵的是該接口的返回值,其返回值也是一個套接字,前面說過TCP通信中存在兩套套接字,一個就是剛才說到的過的監聽套接字,它的職責就是不斷從網絡中獲取連接,因為可能會用很多客戶端想要和服務端通信,當它把連接拿上來之后,調用接口accept,其返回值所產生的套接字就是專門用來處理這條連接,用于進行通信的。這樣做的話整個服務端只需要一個監聽套接字,同時連接只要成功,只需調用接口產生新的套接字再用于通信即可。

int _sock=accept(listen_sock,(struct sockaddr*)&endpoint,&len);

當accept失敗時,該接口返回值也會小于0,需要注意的是此時只是一個連接失敗了,你不能因為這么一個連接失敗了,而把整個服務器給退了,所以要繼續continue。
當accept成功時,我們建立一個新的函數,該函數就是用來專門去處理通信問題的

如下在accept成功之后,加入這樣一句代碼,表示連接上了一臺新的客戶端

然后使用telnet命令,如果你的Centos沒有這個命令,需要進行一定配置,鏈接如下,親測有效

CentOS 7.4安裝telnet服務端

然后使用本地環回測試,使用telnet進行連接,可以發現當一臺主機連接成功時,服務端提示出了相應的訊息

4:recv和send
不同于UDP中的recvfrom和sendto,在TCP通信中,我們盡可能采用的是recv和send來接受和發送

#include <sys/types.h> #include <sys/socket.h>ssize_t recv(int sockfd, void *buf, size_t len, int flags); int send(int s, const void *msg, size_t len, int flags);

(2)客戶端

tcpClient.h

#include <iostream> #include <string> #include <cstring> #include <unistd.h> #include <cstdio> #include <cstdlib> #include <sys/socket.h> #include <sys/types.h> #include <arpa/inet.h> #include <netinet/in.h> using namespace std;class tcpClient { private:string _ip;//服務端IP和端口號int _port;int _sock; public:tcpClient(string ip="127.0.0.1",int port=8080):_ip(ip),_port(port){}void initClient(){_sock=socket(AF_INET,SOCK_STREAM,0);//區別UDPif(_sock < 0){cout<<"套接字創建失敗"<<endl;exit(2);}struct sockaddr_in sev;sev.sin_family=AF_INET;sev.sin_port=htons(_port);sev.sin_addr.s_addr=inet_addr(_ip.c_str());//TCP-connectif(connect(_sock,(struct sockaddr*)&sev,sizeof(sev))!=0){cout<<"connect失敗"<<endl;exit(3);}}void startClient(){char mssage[64];while(1){ size_t s=read(0,mssage,sizeof(mssage)-1);//從標準輸入讀入if(s > 0){mssage[s-1]='\0';//剔除換行符send(_sock,mssage,strlen(mssage),0);ssize_t ret=recv(_sock,mssage,sizeof(mssage)-1,0);if(ret > 0){cout<<"客戶端得到服務端消息"<<mssage<<endl;}}}}~tcpClient(){close(_sock);}};

1:connect

TCP是面向連接,因此對于客戶端來說,它就要調用connect接口連接服務端,如果返回值為0表示連接成功

connect(_sock,(struct sockaddr*)&sev,sizeof(sev))

2:客戶端退出

當客戶端退出時,我們要讓服務器知道客戶端退出,并且關閉已經打開的套接字
那么服務端如何知道客戶端退出了呢,這其實和recv接口的返回值有關

These calls return the number of bytes received, or -1 if an error occurred. In the event of an error, errno is set to indicate theerror. The return value will be 0 when the peer has performed an orderly shutdown.

它的意思就說如果客戶端下線,那么服務端將會接受到0 因此,服務端會有下面這樣代碼

if(s==0) {cout<<"客戶端已經下線"<<endl;close(sock);break; }

二:TCP通信-多進程/線程

使用上面的代碼,利用telnet進行測試,xshell中有多個窗口,代表多個客戶端,第一個客戶端連接后,服務端的確打印出了相關訊息,但是第二個和第三個在連接時卻沒有打印出信息

問題的原因就是咋們編寫的服務器目前是一個單進程版的服務器,當第一個主機連接時,由于沒有發送數據,因此進程會被阻塞在service函數中,到時后面的客戶端連接不上

(1)多進程版本

tcpServer.h

#include <iostream> #include <signal.h> #include <string> #include <unistd.h> #include <cstdio> #include <cstring> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> using namespace std;#define BACKLOG 5class tcpServer { private:int _port;int listen_sock; public:tcpServer(int port=8080):_port(port){}void initServer(){signal(SIGCHLD,SIG_IGN);//讓子進程自己銷毀listen_sock=socket(AF_INET,SOCK_STREAM,0);//區別UDP,TCP采用流式套接字if(listen_sock < 0){cout<<"套接字創建失敗"<<endl;exit(2);}struct sockaddr_in local;local .sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=htonl(INADDR_ANY);if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0){cout<<"綁定失敗"<<endl;exit(3);}//開啟監聽if(listen(listen_sock,BACKLOG) < 0){cout<<"綁定失敗"<<endl;exit(4);}}void Service(int sock){char buffer[1024];while(1){size_t s=recv(sock,buffer,sizeof(buffer)-1,0);if(s > 0){buffer[s]='\0';cout<<"服務端收到客戶端的消息:"<<buffer<<endl;send(sock,buffer,strlen(buffer),0);}if(s==0){cout<<"客戶端已經下線"<<endl;close(sock);break;}}}void startServer(){sockaddr_in endpoint;while(1){//acceptsocklen_t len=sizeof(endpoint);int _sock=accept(listen_sock,(struct sockaddr*)&endpoint,&len);if(_sock < 0){cout<<"accept失敗"<<endl;continue;}string client_info=inet_ntoa(endpoint.sin_addr);client_info+=":";client_info+=to_string(ntohs(endpoint.sin_port));cout<<"一臺新的客戶端已經連接:"<<client_info<<endl;pid_t id=fork();if(id==0)//子進程{close(listen_sock); //子進程會以父進程為模板,復制父進程PCB,所以對于子進程來說,可以關閉的它的listen_sock,盡量節省資源Service(_sock);exit(0);//子進程處理完畢}//對于父進程它只關心監聽套接字,所以可以把父進程的sock關閉,而且是必須關掉,因為這個sock對它沒用了close(_sock);}}~tcpServer(){close(listen_sock);} };

以上代碼中關于父子進程之間的關系,以及進程等待這里就不細談了,詳見

Linux系統編程

再次強調,對于子進程,它可以關閉監聽套接字,因為子進程是用來通信的,它只關心sock,對于父進程則必須要關閉sock,只保留監聽套接字,否則客戶端連接的越多,系統資源將會被耗盡

再次測試,可以發現多個客戶端可以同時連接服務器

(2)多線程版本

在Linux系統編程那一部分我們詳細說過多線程和多進程的優缺點,創建進程的代價遠遠高于創建線程,因此多線程版本如下

#include <iostream> #include <pthread.h> #include <signal.h> #include <string> #include <unistd.h> #include <cstdio> #include <cstring> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> using namespace std;#define BACKLOG 5class tcpServer { private:int _port;int listen_sock; public:tcpServer(int port=8080):_port(port){}void initServer(){listen_sock=socket(AF_INET,SOCK_STREAM,0);//區別UDP,TCP采用流式套接字if(listen_sock < 0){cout<<"套接字創建失敗"<<endl;exit(2);}struct sockaddr_in local;local .sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=htonl(INADDR_ANY);if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0){cout<<"綁定失敗"<<endl;exit(3);}//開啟監聽if(listen(listen_sock,BACKLOG) < 0){cout<<"綁定失敗"<<endl;exit(4);}}static void Service(int sock){char buffer[1024];while(1){size_t s=recv(sock,buffer,sizeof(buffer)-1,0);if(s > 0){buffer[s]='\0';cout<<"服務端收到客戶端的消息:"<<buffer<<endl;send(sock,buffer,strlen(buffer),0);}if(s==0){cout<<"客戶端已經下線"<<endl;close(sock);break;}}}static void* Route(void* args)//線程路線{pthread_detach(pthread_self());int* p=(int*)args;int sock=*p;Service(sock);return NULL;}void startServer(){sockaddr_in endpoint;while(1){//acceptsocklen_t len=sizeof(endpoint);int _sock=accept(listen_sock,(struct sockaddr*)&endpoint,&len);if(_sock < 0){cout<<"accept失敗"<<endl;continue;}string client_info=inet_ntoa(endpoint.sin_addr);client_info+=":";client_info+=to_string(ntohs(endpoint.sin_port));cout<<"一臺新的客戶端已經連接:"<<client_info<<endl;pthread_t tid;int* p=new int(sock);//在堆上開辟,p是棧私有的,防止出現bugpthread_create(&tid,nullptr,Route,(void*)&_sock);}}~tcpServer(){close(listen_sock);} };

效果如下

(3)線程池版本

關于線程部分,可以查看

Linux系統編程41:多線程之線程池的概念及實現

服務器大致邏輯為:

  • 初始化服務器,服務器初始化時創建線程池
  • 啟動服務器,創建任務,將任務放進線程池的任務隊列中
  • 由于任務的加入,喚醒了線程池中的線程,線程拿到任務,調用任務的工作接口,進行工作
  • 工作完畢調用析構函數,釋放套接字
  • 下面的代碼簡單的實現了客戶端發送英文單詞,服務端進行翻譯的過程

    thread_pool.h

    #pragma once #include <iostream> #include <pthread.h> #include <signal.h> #include <string> #include <unistd.h> #include <cstdio> #include <cstring> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <queue> #include <map> using namespace std;struct Task//任務 { private:int _sock;//套接字static map<string,string> dict;//字典 public:Task(){}~Task(){close(_sock);}Task(int sock):_sock(sock){dict.insert(pair<string,string>("apple","蘋果"));dict.insert(pair<string,string>("melon","西瓜"));dict.insert(pair<string,string>("orange","橘子"));}void work()//獲得任務后在這里進行判斷{char buffer[1024];size_t s=recv(_sock,buffer,sizeof(buffer)-1,0);if(s > 0){buffer[s]='\0';string key=buffer;//拿到用戶的鍵值cout<<"服務端收到客戶端的消息:"<<buffer<<endl;send(_sock,dict[key].c_str(),dict[key].size(),0);//給用戶返回翻譯結果}if(s==0){cout<<"客戶端已經下線"<<endl;}} };map<string,string>Task:: dict;class ThreadPool//線程池 { private:bool _thread_quit_flag;//線程退出標志,如果主線程發送了信號,就將其置為trueint _thread_num;//線程池線程數量queue<Task*> q;//任務隊列,存放指針pthread_mutex_t lock;//保護任務隊列的鎖pthread_cond_t cond;//條件變量,當沒有任務時線程池的線程掛起,當有任務時喚醒線程池線程public:void ThreadLock(){//鎖pthread_mutex_lock(&lock);}void ThreadUnlock(){//解鎖pthread_mutex_unlock(&lock);}bool IsEmpty(){//判斷任務隊列為空return q.size()==0;}void Threadwait(){//沒有任務線程池的線程掛起pthread_cond_wait(&cond,&lock);}void Threadwakeup(){//當主線程添加了任務就喚醒線程池線程pthread_cond_signal(&cond);}void ThreadWakeAll(){//最后發送退出信號時,將所有線程喚醒,然后讓其退出pthread_cond_broadcast(&cond);//喚醒所有線程}void ThreadQuit(){//線程退出函數pthread_exit(nullptr);}public:ThreadPool(int thread_num=5):_thread_quit_flag(false),_thread_num(thread_num)//flag默認設置為flase,不退出{pthread_mutex_init(&lock,nullptr);pthread_cond_init(&cond,nullptr);}~ThreadPool(){pthread_mutex_destroy(&lock);pthread_cond_destroy(&cond);}static void* Route(void* arg)//所有的線程都要執行這樣的流程:先判斷是否有任務,有就執行,沒有就掛起{pthread_detach(pthread_self());//線程 ThreadPool* this_p=(ThreadPool*)arg;//某個線程的this指針while(1){this_p->ThreadLock();//鎖定任務隊列while(this_p->_thread_quit_flag==false && this_p->IsEmpty())//如果沒有發出退出信號并且隊列為空,那么就讓線程掛起{this_p->Threadwait();//當線程蘇醒時(可能接收到了廣播信號或者位于線程隊列它被下一個喚醒了),就會從掛起的地方醒來,繼續向下執行}if((this_p->_thread_quit_flag==false && !this_p->IsEmpty()) || (this_p->_thread_quit_flag==true && !this_p->IsEmpty()))//醒來的原因不管是否是因為接收到了退出信號,反正只要有任務就得先執行完{Task* t;this_p->Get(&t);this_p->ThreadUnlock();//注意不要在臨界資源內做任務,效率很低t->work();//獲得任務后進行計算delete t;//任務結束關閉套接字,調用任務析構函數關閉套接字}//如果接受到信號且隊列已經Wie空了,那么就退出線程else if(this_p->_thread_quit_flag==true && this_p->IsEmpty())//如果醒來的原因是因為接受到了退出信號,而且任務隊列中已經沒有任務了,那么就退出線程{this_p->ThreadUnlock();this_p->ThreadQuit();//退出時不要忘記解鎖,否則將來只有一個線程能推出,其余線程無法退出}} }void ThreadPoolInit()//風險操作不要在構造函數中寫,該函數用于創建線程池線程,并讓線程執行Route流程{pthread_t t;for(int i=0;i < _thread_num;i++){pthread_create(&t,nullptr,Route,this);//這里最后一個參數傳入了this指針,相應的Route函數也是靜態成員函數//如果不傳入this指針,那么當這個參數就會和非靜態成員函數搶this指針的位置,因此會造成參數過多的問題//所以在這里傳入this,然后在形參中使用this調用自己的成員函數即可}}void ThreadPoolQuit()//線程池退出{_thread_quit_flag=true;//將結束標志置為trueThreadWakeAll();//喚醒所有線程}void Get(Task** t)//線程獲取任務{Task* out=q.front();//注意是指針q.pop();*t=out;//這是解引用}void Put(Task& t)//主線程放任務{ThreadLock();q.push(&t);//注意這是取地址ThreadUnlock();Threadwakeup();//當放了一個任務后立馬喚醒線程,這里一般情況下不要喚醒所有線程,會產生掠群效應,影響穩定性} };

    tcp_server.h

    #include "thread_pool.h" #define BACKLOG 5class tcpServer { private:int _port;int listen_sock;ThreadPool* _th; public:tcpServer(int port=8080):_port(port),_th(nullptr){}void initServer(){listen_sock=socket(AF_INET,SOCK_STREAM,0);//區別UDP,TCP采用流式套接字if(listen_sock < 0){cout<<"套接字創建失敗"<<endl;exit(2);}struct sockaddr_in local;local .sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=htonl(INADDR_ANY);if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0){cout<<"綁定失敗"<<endl;exit(3);}//開啟監聽if(listen(listen_sock,BACKLOG) < 0){cout<<"綁定失敗"<<endl;exit(4);}_th=new ThreadPool();_th->ThreadPoolInit();//初始化線程池}static void Service(int sock){char buffer[1024];while(1){size_t s=recv(sock,buffer,sizeof(buffer)-1,0);if(s > 0){buffer[s]='\0';cout<<"服務端收到客戶端的消息:"<<buffer<<endl;send(sock,buffer,strlen(buffer),0);}if(s==0){cout<<"客戶端已經下線"<<endl;close(sock);break;}}}static void* Route(void* args){pthread_detach(pthread_self());int* p=(int*)args;int sock=*p;Service(sock);return NULL;}void startServer(){sockaddr_in endpoint;while(1){//acceptsocklen_t len=sizeof(endpoint);int _sock=accept(listen_sock,(struct sockaddr*)&endpoint,&len);if(_sock < 0){cout<<"accept失敗"<<endl;continue;}string client_info=inet_ntoa(endpoint.sin_addr);client_info+=":";client_info+=to_string(ntohs(endpoint.sin_port));cout<<"一臺新的客戶端已經連接:"<<client_info<<endl;//構建任務,放進線程池Task* t=new Task(_sock);_th->Put(*t);}}~tcpServer(){close(listen_sock);}};

    tcp_client.h

    #include <iostream> #include <string> #include <cstring> #include <unistd.h> #include <cstdio> #include <cstdlib> #include <sys/socket.h> #include <sys/types.h> #include <arpa/inet.h> #include <netinet/in.h> using namespace std;class tcpClient { private:string _ip;int _port;int _sock; public:tcpClient(string ip="127.0.0.1",int port=8080):_ip(ip),_port(port){}void initClient(){_sock=socket(AF_INET,SOCK_STREAM,0);if(_sock < 0){cout<<"套接字創建失敗"<<endl;exit(2);}struct sockaddr_in sev;sev.sin_family=AF_INET;sev.sin_port=htons(_port);sev.sin_addr.s_addr=inet_addr(_ip.c_str());if(connect(_sock,(struct sockaddr*)&sev,sizeof(sev))!=0){cout<<"connect失敗"<<endl;exit(3);}}void startClient(){char mssage[64];while(1){ cout<<"請輸入信息**:";fflush(stdout);size_t s=read(0,mssage,sizeof(mssage)-1);//從標準輸入讀入if(s > 0){mssage[s-1]='\0';//剔除換行符send(_sock,mssage,strlen(mssage),0);ssize_t ret=recv(_sock,mssage,sizeof(mssage)-1,0);if(ret > 0){cout<<"客戶端得到服務端消息"<<mssage<<endl;}else if(ret ==0){break;}}}}~tcpClient(){close(_sock);} };

    效果如下

    新人創作打卡挑戰賽發博客就能抽獎!定制產品紅包拿不停!

    總結

    以上是生活随笔為你收集整理的2-4:套接字(Socket)编程之TCP通信的全部內容,希望文章能夠幫你解決所遇到的問題。

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