簡(jiǎn)單的TCP網(wǎng)絡(luò)程序
一、服務(wù)器創(chuàng)建套接字
與前邊的UDP網(wǎng)絡(luò)程序相同,創(chuàng)建套接字的接口都是socket,下邊對(duì)socket接口進(jìn)行介紹:
協(xié)議家族選擇AF_INET,因?yàn)槲覀円M(jìn)行網(wǎng)絡(luò)通信。
而第二個(gè)參數(shù),為服務(wù)類型,傳入SOCK_STREAM,我們編寫(xiě)TCP程序,所以要選擇流式的服務(wù)。
第三個(gè)參數(shù)默認(rèn)傳入0,由前兩個(gè)參數(shù)就可以推出這是基于TCP的網(wǎng)絡(luò)程序。
// 創(chuàng)建套接字
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0)
{
logMessage(FATAL, "create socket error %d-%s", errno, strerror(errno));
exit(2);
}
socket接口如果創(chuàng)建成功返回0,失敗返回-1,并且錯(cuò)誤碼被設(shè)置,所以當(dāng)返回值小于0時(shí)退出程序。
二、服務(wù)器綁定套接字
還是與UDP相同,綁定套接字需要bind接口,我們?cè)俅螌?duì)bind接口進(jìn)行學(xué)習(xí):
第一個(gè)參數(shù)傳入前邊創(chuàng)建的套接字,也就是一個(gè)文件描述符。
第二個(gè)參數(shù)是一個(gè)sockaddr類型結(jié)構(gòu)體的地址,內(nèi)邊存儲(chǔ)著要綁定的IP和端口號(hào)的相關(guān)信息。
第三個(gè)參數(shù)為結(jié)構(gòu)體的大小。
// bind綁定套接字
struct sockaddr_in local;
// bzero((void*)&local,sizeof(local));
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(_port);
inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);
// local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
if (bind(_sock, (struct sockaddr *)&local, (socklen_t)sizeof(local)) < 0)
{
logMessage(FATAL, "bind socket error %d-%s", errno, strerror(errno));
exit(3);
}
但是同時(shí)要注意網(wǎng)絡(luò)序列和主機(jī)序列的轉(zhuǎn)換,并且在處理IP地址時(shí),也要注意到點(diǎn)分十進(jìn)制與二進(jìn)制的轉(zhuǎn)換。
三、服務(wù)器監(jiān)聽(tīng)
由于TCP協(xié)議是需要連接的,而UDP是不需要連接的,所以在對(duì)TCP的服務(wù)器進(jìn)行創(chuàng)建,綁定套接字之后,必須進(jìn)行監(jiān)聽(tīng)操作,使服務(wù)器處于監(jiān)聽(tīng)狀態(tài)。這就例如:
一個(gè)商店老板,即使這會(huì)沒(méi)有人來(lái)買東西,也必須坐在店里邊,處于監(jiān)聽(tīng)狀態(tài),一旦有人來(lái)買東西,就可以立馬為客戶服務(wù)。
一旦listen調(diào)用成功,服務(wù)器就會(huì)處于監(jiān)聽(tīng)狀態(tài)。
sockfd:
需要設(shè)置為監(jiān)聽(tīng)狀態(tài)的套接字對(duì)應(yīng)的文件描述符。backlog:
全連接隊(duì)列的最大長(zhǎng)度。如果有多個(gè)客戶端同時(shí)發(fā)來(lái)連接請(qǐng)求,此時(shí)未被服務(wù)器處理的連接就會(huì)放入連接隊(duì)列,該參數(shù)代表的就是這個(gè)全連接隊(duì)列的最大長(zhǎng)度,一般不要設(shè)置太大,設(shè)置為5或10即可。返回值:
如果監(jiān)聽(tīng)失敗返回-1,并且錯(cuò)誤碼被設(shè)置,成功返回0.
// 監(jiān)聽(tīng)
if (listen(_sock, gbacklog) < 0)
{
logMessage(FATAL, "listen socket error %d-%s", errno, strerror(errno));
exit(4);
}
logMessage(NORMAL, "init success,sockfd: %d", _sock);
當(dāng)監(jiān)聽(tīng)完成之后,服務(wù)器的初始化才算完成。
四、服務(wù)器獲取連接
當(dāng)服務(wù)器初始化完成之后,此時(shí)就要讓客戶端來(lái)連接,必須通過(guò)accept來(lái)獲取連接,當(dāng)客戶端發(fā)送連接請(qǐng)求之后,服務(wù)器和客戶端的連接才正式完成。
參數(shù):
sockfd:監(jiān)聽(tīng)套接字的文件描述符
addr:對(duì)端網(wǎng)絡(luò)的相關(guān)信息結(jié)構(gòu)體,例如IP,端口號(hào),協(xié)議家族等
addrlen:addr結(jié)構(gòu)體的大小
返回值:
accept的返回值有一些不同,如果返回成功,這些系統(tǒng)調(diào)用返回一個(gè)非負(fù)整數(shù),它是一個(gè)描述符對(duì)于接受的套接字。如果出現(xiàn)錯(cuò)誤,則返回-1,并適當(dāng)?shù)卦O(shè)置errno。
那么這個(gè)返回值是什么意思呢?為什么會(huì)有兩個(gè)文件描述符,他們之間有什么關(guān)系?
當(dāng)我們使用accpet進(jìn)行連接時(shí),是通過(guò)監(jiān)聽(tīng)套接字進(jìn)行連接的,但是當(dāng)連接上對(duì)端網(wǎng)絡(luò)之后,不是監(jiān)聽(tīng)套接字來(lái)提供服務(wù)的,而是返回成功之后,會(huì)返回一個(gè)套接字的文件描述符,是由該服務(wù)套接字提供服務(wù)的。
- 監(jiān)聽(tīng)套接字:用于獲取客戶端發(fā)來(lái)的連接請(qǐng)求。accept函數(shù)會(huì)不斷從監(jiān)聽(tīng)套接字當(dāng)中獲取新連接。
- accept函數(shù)返回的套接字:用于為本次accept獲取到的連接提供服務(wù)。監(jiān)聽(tīng)套接字的任務(wù)只是不斷獲取新連接,而真正為這些連接提供服務(wù)的套接字是accept函數(shù)返回的套接字,而不是監(jiān)聽(tīng)套接字。
下邊通過(guò)一個(gè)例子來(lái)解釋他們之間的關(guān)系:
當(dāng)我們前往西安旅游時(shí),一定想嘗一嘗正宗的羊肉泡饃,有一家店,服務(wù)員張三非常熱情,一定在門口招呼路上的游客進(jìn)去,當(dāng)有一個(gè)游客準(zhǔn)備進(jìn)入餐館吃飯時(shí),張三就會(huì)喊一聲,李四來(lái)人了,快出來(lái)招呼,但是張三又回到門口,繼續(xù)讓來(lái)往的游客進(jìn)入餐館,當(dāng)下一個(gè)游客進(jìn)入餐館時(shí),張三就說(shuō),王五來(lái)人了快來(lái)招呼人,此時(shí)服務(wù)顧客的人就是王五,而張三繼續(xù)去外邊找客人。
此處的張三我們就可以認(rèn)為是監(jiān)聽(tīng)套接字,主要功能就是不斷的在外邊找顧客,讓顧客進(jìn)入店內(nèi),也就是不斷的獲取新連接,而后邊的李四王五趙六等等就相當(dāng)于accept返回的服務(wù)套接字,他們才是為對(duì)端提供服務(wù)的。
// 建立連接
struct sockaddr_in src;
socklen_t len = sizeof(src);
int fd = accept(_sock, (struct sockaddr *)&src, &len);
if (fd < 0)
{
logMessage(FATAL, "accept error %d-%s", errno, strerror(errno));
continue;
}
五、服務(wù)器處理請(qǐng)求
通過(guò)以上的步驟,創(chuàng)建套接字,綁定套接字,監(jiān)聽(tīng),獲取連接之后,當(dāng)客戶端對(duì)服務(wù)器進(jìn)行連接之后,服務(wù)器就可以處理客戶端發(fā)來(lái)的請(qǐng)求。
void service(int fd, const std::string &client_ip, const uint16_t &client_port)
{
char buffer[1024];
while (1)
{
ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
std::cout<<client_ip << ":" << client_port << "# " << buffer << std::endl;
}
else if (s == 0)
{
logMessage(ERROR, "%s-%d client close fd,me too!", client_ip.c_str(), client_port);
break;
}
else
{
logMessage(FATAL, "read error %d-%s", errno, strerror(errno));
break;
}
write(fd, buffer, strlen(buffer));
}
close(fd);
}
由于套接字在系統(tǒng)層面來(lái)看,就是打開(kāi)的文件,所以對(duì)文件進(jìn)行讀寫(xiě)就可以使用我們之前學(xué)習(xí)過(guò)的 read和write接口。
read
讀取數(shù)據(jù)時(shí),使用read接口。
參數(shù):
fd:文件描述符,表示從哪一個(gè)套接字中讀取
buf:數(shù)據(jù)的存儲(chǔ)位置,把數(shù)據(jù)讀取到哪一個(gè)數(shù)組中
count:讀取數(shù)據(jù)的大小返回值:
當(dāng)讀取成功時(shí),返回讀取到的字節(jié)數(shù),當(dāng)寫(xiě)端關(guān)閉時(shí),返回0,當(dāng)讀取錯(cuò)誤時(shí),返回小于0.
當(dāng)返回值為0時(shí),表示讀取對(duì)端關(guān)閉了?
網(wǎng)絡(luò)通信與進(jìn)程間通信類似,和之前對(duì)文件讀取寫(xiě)出相同:
- 寫(xiě)端進(jìn)程不寫(xiě),讀端進(jìn)程一直讀,此時(shí)讀端進(jìn)程就會(huì)被掛起,因?yàn)榇藭r(shí)數(shù)據(jù)沒(méi)有就緒。
- 讀端進(jìn)程不讀,寫(xiě)端進(jìn)程一直寫(xiě),此時(shí)當(dāng)緩沖區(qū)被寫(xiě)滿后寫(xiě)端進(jìn)程就會(huì)被掛起,因?yàn)榇藭r(shí)空間沒(méi)有就緒。
- 寫(xiě)端進(jìn)程將數(shù)據(jù)寫(xiě)完后將寫(xiě)端關(guān)閉,此時(shí)當(dāng)讀端進(jìn)程將管道當(dāng)中的數(shù)據(jù)讀完后就會(huì)讀到0。
- 讀端進(jìn)程將讀端關(guān)閉,此時(shí)寫(xiě)端進(jìn)程就會(huì)被操作系統(tǒng)殺掉,因?yàn)榇藭r(shí)寫(xiě)端進(jìn)程寫(xiě)入的數(shù)據(jù)不會(huì)被讀取。
此處的情況就是寫(xiě)端也就是客戶端將數(shù)據(jù)寫(xiě)完后將寫(xiě)端關(guān)閉,此時(shí)讀端也就是服務(wù)器將數(shù)據(jù)讀完之后就會(huì)讀到0,所以返回值為0。
write
寫(xiě)入數(shù)據(jù)到網(wǎng)絡(luò)時(shí),需要使用write接口。
參數(shù):
fd:寫(xiě)端套接字的文件描述符
buf:需要寫(xiě)入的數(shù)據(jù)
count:需要寫(xiě)入數(shù)據(jù)的字節(jié)數(shù)
返回值:
寫(xiě)入成功返回寫(xiě)入的字節(jié)數(shù),寫(xiě)入失敗返回-1,同時(shí)錯(cuò)誤碼被設(shè)置。
六、對(duì)服務(wù)器進(jìn)行簡(jiǎn)單測(cè)試
當(dāng)服務(wù)器初始化已經(jīng)處理請(qǐng)求都完成之后,雖然還沒(méi)有實(shí)現(xiàn)客戶端,但是也可以telnet
指令遠(yuǎn)程連接該服務(wù)器,實(shí)現(xiàn)請(qǐng)求處理服務(wù):
第一步:
運(yùn)行服務(wù)器,必須加上端口號(hào),此時(shí)處于監(jiān)聽(tīng)狀態(tài),等待客戶端連接。
此時(shí)可以使用netstat
指令觀察該套接字的狀態(tài):
可以發(fā)現(xiàn)此時(shí)的服務(wù)器處于listen狀態(tài)。
第二步:
使用telnet指令對(duì)服務(wù)器進(jìn)行連接。
第三步:
對(duì)服務(wù)器進(jìn)行請(qǐng)求。
七、客戶端創(chuàng)建套接字
與前邊創(chuàng)建套接字沒(méi)有什么區(qū)別,注意的就是使用流式傳輸。
客戶端是不需要綁定IP和端口號(hào)的,在客戶端在連接時(shí),系統(tǒng)會(huì)自動(dòng)給客戶端分配。
客戶端也不需要監(jiān)聽(tīng),因?yàn)榭蛻舳瞬粫?huì)被主動(dòng)連接。
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
std::string server_ip = argv[1];
uint16_t server_port = atoi(argv[2]);
// 創(chuàng)建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
}
八、客戶端連接服務(wù)器
客戶端要發(fā)送請(qǐng)求時(shí),必須要知道服務(wù)器的IP地址和端口號(hào),所以我們使用命令行參數(shù)的方式,將服務(wù)器的Ip地址和端口號(hào)傳給客戶端,客戶端接收之后之后,將IP地址和端口號(hào)傳入addr結(jié)構(gòu)體中,然后使用connect接口進(jìn)行連接。
struct sockaddr_in peer;
memset(&peer,'\0', sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(server_port);
peer.sin_addr.s_addr = inet_addr(server_ip.c_str());
// 連接套接字
if (connect(sock, (struct sockaddr *)&peer, (socklen_t)sizeof(peer)) < 0)
{
std::cerr << "connect error" << std::endl;
exit(3);
}
std::cout << "connect success" << std::endl;
connect接口如果調(diào)用成功,客戶端會(huì)被隨機(jī)分配一下端口號(hào),只要可以唯一標(biāo)識(shí)客戶端即可。
九、客戶端發(fā)起請(qǐng)求
客戶端與服務(wù)器連接成功之后,使用send接口發(fā)送數(shù)據(jù),如果發(fā)送成功,返回值大于0。當(dāng)發(fā)送成功之后,使用recv接口接收數(shù)據(jù),最后在收到的數(shù)據(jù)后加上’\0’,將字符串回顯。
while (true)
{
std::string line;
std::cout << "請(qǐng)輸入# " << std::endl;
getline(std::cin, line);
if (line == "quit")
break;
ssize_t s = send(sock,line.c_str(),line.size(),0);
if(s>0)
{
char buffer[1024];
ssize_t s = recv(sock,buffer,sizeof(buffer)-1,0);
if(s > 0)
{
buffer[s]=0;
std::cout<<"回顯#"<<buffer<<std::endl;
}
else if(s==0)
{
break;
}
else
{
break;
}
}
}
十、服務(wù)器客戶端測(cè)試
服務(wù)端
客戶端
當(dāng)在客戶端發(fā)起請(qǐng)求之后,客戶端會(huì)與服務(wù)器建立連接,此時(shí)再次輸入數(shù)據(jù),服務(wù)器會(huì)對(duì)收到的數(shù)據(jù)進(jìn)行回顯。
多進(jìn)程的TCP服務(wù)器
如果是單進(jìn)程的服務(wù)器,當(dāng)多個(gè)客戶端同時(shí)啟動(dòng),服務(wù)器只能處理一個(gè)客戶端的請(qǐng)求,只有當(dāng)?shù)谝粋€(gè)客戶端退出之后,才會(huì)收到第二個(gè)客戶端的請(qǐng)求。
為什么可以使用多進(jìn)程
由于創(chuàng)建子進(jìn)程后,子進(jìn)程會(huì)繼承父進(jìn)程的文件描述符等信息,所以父進(jìn)程創(chuàng)建的套接字也會(huì)被子進(jìn)程繼承下來(lái),當(dāng)我們使用多進(jìn)程時(shí),子進(jìn)程就可以看到建立鏈接的文件描述符,并且當(dāng)某一個(gè)進(jìn)程處理完畢之后關(guān)閉文件描述符,也不會(huì)影響到其他的進(jìn)程,因?yàn)楦缸舆M(jìn)程具有獨(dú)立性,在修改時(shí)會(huì)進(jìn)行寫(xiě)時(shí)拷貝。
等待子進(jìn)程問(wèn)題
在子進(jìn)程處理請(qǐng)求完畢之后,父進(jìn)程必須等待子進(jìn)程,要不然就會(huì)造成僵尸問(wèn)題,會(huì)造成內(nèi)存泄露,等待子進(jìn)程有兩種方式:
- 阻塞等待
- 非阻塞等待
如果使用阻塞等待,那么說(shuō)明父進(jìn)程必須在等待第一個(gè)子進(jìn)程服務(wù)完畢之后才可以處理下一個(gè)請(qǐng)求,本質(zhì)上還是進(jìn)行串行操作,并沒(méi)有真正實(shí)現(xiàn)多進(jìn)程。
而如果使用非阻塞等待,雖然可以再進(jìn)行其他的連接,但是必須不斷的檢測(cè)子進(jìn)程是否退出。
為了解決以上的問(wèn)題,我們可以采取兩種方法:
- 對(duì)SIGCHLD進(jìn)行自定義捕捉,主動(dòng)忽略SIGCHLD信號(hào),當(dāng)子進(jìn)程退出時(shí),就會(huì)主動(dòng)釋放僵尸進(jìn)程,父進(jìn)程不會(huì)進(jìn)行等待。
- 創(chuàng)建子進(jìn)程,再讓子進(jìn)程創(chuàng)建子進(jìn)程,讓孫子進(jìn)程進(jìn)行服務(wù),將子進(jìn)程直接退出,當(dāng)孫子進(jìn)程處理完畢之后,成為孤兒進(jìn)程被操作系統(tǒng)回收,所以父進(jìn)程不需要進(jìn)行等待。
一、忽略SIGCHLD信號(hào)
signal(SIGCHLD, SIG_IGN); // 對(duì)SIGCHLD,主動(dòng)忽略SIGCHLD信號(hào),子進(jìn)程退出的時(shí)候,會(huì)自動(dòng)釋放自己的僵尸狀態(tài).
//version1.0多進(jìn)程版,對(duì)信號(hào)進(jìn)行忽略
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
close(_sock);
service(fd, client_ip, client_port);
exit(0);
}
close(fd);
二、孫子進(jìn)程提供服務(wù)
先創(chuàng)建子進(jìn)程,再讓子進(jìn)程創(chuàng)建子進(jìn)程,讓孫子進(jìn)程提供服務(wù),但是將子進(jìn)程退出,當(dāng)孫子進(jìn)程提供完服務(wù)之后,被操作系統(tǒng)回收。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-578394.html
void service(int fd, const std::string &client_ip,
const uint16_t &client_port)
{
char buffer[1024];
while (1)
{
ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
std::cout<<client_ip << ":" << client_port << "# " << buffer << std::endl;
}
else if (s == 0)
{
logMessage(ERROR, "%s-%d client close fd,me too!", client_ip.c_str(), client_port);
break;
}
else
{
logMessage(FATAL, "read error %d-%s", errno, strerror(errno));
break;
}
write(fd, buffer, strlen(buffer));
}
close(fd);
}
//version1 .1多進(jìn)程版,使用孫子進(jìn)程進(jìn)行服務(wù)
pid_t id = fork();
if (id == 0)
{
close(_sock);
if (fork() > 0)
exit(0);
else
{
service(fd, client_ip, client_port);
exit(0);
}
}
waitpid(id, nullptr, 0);
close(fd);
多線程TCP服務(wù)器
服務(wù)器為了同時(shí)給多個(gè)客戶端提供服務(wù),不僅可以使用多進(jìn)程來(lái)進(jìn)行服務(wù),也可以使用多線程來(lái)提供服務(wù)。
由于線程的回調(diào)函數(shù)中需要多個(gè)變量,所以我們將需要的IP,端口號(hào),文件描述符寫(xiě)入一個(gè)類中,將實(shí)例化的對(duì)象指針傳入回調(diào)函數(shù),在回調(diào)函數(shù)中使用pthread_detach接口實(shí)現(xiàn)線程分離,主線程這邊就不需要進(jìn)行join回收線程了。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-578394.html
//服務(wù)函數(shù)
void service(int fd, const std::string &client_ip,
const uint16_t &client_port)
{
char buffer[1024];
while (1)
{
ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
std::cout << client_ip << ":" << client_port << "# " << buffer << std::endl;
}
else if (s == 0)
{
logMessage(ERROR, "%s-%d client close fd,me too!", client_ip.c_str(), client_port);
break;
}
else
{
logMessage(FATAL, "read error %d-%s", errno, strerror(errno));
break;
}
write(fd, buffer, strlen(buffer));
}
close(fd);
}
//線程數(shù)據(jù)
class pthreadData
{
public:
int _sock;
std::string _ip;
uint16_t _port;
};
// version2多線程版
pthreadData *pd = new pthreadData();
pd->_port = client_port;
pd->_sock = fd;
pthread_t tid;
pthread_create(&tid, nullptr, Routine, pd);
//線程回調(diào)函數(shù)
static void *Routine(void *args)
{
pthread_detach(pthread_self());
pthreadData *pd = (pthreadData *)args;
std::string client_ip = pd->_ip;
uint16_t client_port = pd->_port;
int sock = pd->_sock;
service(sock, client_ip, client_port);
return nullptr;
}
到了這里,關(guān)于【網(wǎng)絡(luò)編程】網(wǎng)絡(luò)編程套接字(三)TCP網(wǎng)絡(luò)程序的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!