網(wǎng)絡(luò)字節(jié)序——TCP接口及其實現(xiàn)簡單TCP服務(wù)器
簡單TCP服務(wù)器的實現(xiàn)
- TCP區(qū)別于UDP在于要設(shè)置套接字為監(jiān)控狀態(tài),即TCP是面向鏈接,因此TCP套接字需要設(shè)置為監(jiān)聽狀態(tài)
void initserver()
{
//1.創(chuàng)建套接字
_listensock=socket(AF_INET,SOCK_STREAM,0);
if(_listensock<0)
{
logMessage(FATAL,"create listensocket error");
exit(SOCK_ERR);
}
logMessage(NORMAL, "create socket success: %d", _listensock);
//2.bind ip和port
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//綁定失敗
{
logMessage(FATAL,"bind error");
exit(BIND_ERR);
}
logMessage(NORMAL,"bind success");
//3.將套接字設(shè)置為監(jiān)聽模式
if(listen(_listensock,0)<0)
{
logMessage(FATAL,"listen error");
exit(LISTEN_ERR);
}
logMessage(NORMAL,"listen success");
}
socket函數(shù)原型
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
-
domain
表示協(xié)議族,常用的有AF_INET
(IPv4)和AF_INET6
(IPv6)。 -
type
表示Socket類型,常用的有SOCK_STREAM
(TCP)和SOCK_DGRAM
(UDP)。 -
protocol
通??梢栽O(shè)置為 0,讓系統(tǒng)根據(jù)domain
和type
來選擇合適的協(xié)議。 -
socket()打開一個網(wǎng)絡(luò)通訊端口,如果成功的話,就像open()一樣返回一個文件描述符
-
應(yīng)用程序可以像讀寫文件一樣通過socket函數(shù)用read/write在網(wǎng)絡(luò)上收發(fā)數(shù)據(jù)
bind函數(shù)原型
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
sockfd
是socket描述符。 -
addr
是一個struct sockaddr
結(jié)構(gòu)體,包含要綁定的IP地址和端口信息。 -
addrlen
是addr
結(jié)構(gòu)體的長度。因為addr結(jié)構(gòu)體可以接受多種協(xié)議的sockaddr結(jié)構(gòu)體,因此要傳其結(jié)構(gòu)體的長度 -
bind()成功返回0,失敗返回-1。
-
bind()的作用是將參數(shù)sockfd和myaddr綁定在一起, 使sockfd這個用于網(wǎng)絡(luò)通訊的文件描述符監(jiān)聽addr所描述的地址和端口號;
listen函數(shù)原型
int listen(int sockfd, int backlog);
-
sockfd
是socket描述符,指用于進行網(wǎng)絡(luò)監(jiān)聽的文件描述符 -
backlog
表示等待連接隊列的最大長度。 - listen成功返回0,失敗返回-1
- listen函數(shù)將使得sockfd處于監(jiān)聽狀態(tài),并且允許backlog個客戶端處于連接等待狀態(tài),當收到多于backlog個客戶端的的連接請求則選擇忽略。
- 實際上listen函數(shù)告訴操作系統(tǒng)指定的套接字sockfd處于監(jiān)聽狀態(tài),該套接字開始等待其他計算機通過網(wǎng)絡(luò)與其建立連接,一旦有連接請求到達,操作系統(tǒng)會將連接請求放入連接隊列中,連接隊列的最大長度為backlog,連接隊列是一個存放連接請求的緩沖區(qū),如果隊列已滿新的連接請求將會被拒絕。即當一個套接字處于監(jiān)聽狀態(tài)時,它不直接處理數(shù)據(jù)傳輸,而是等待其他計算機發(fā)起連接。
總的來說initserver函數(shù)作用是先創(chuàng)建套接字,然后填充指定的端口號和ip,并將套接字設(shè)置為監(jiān)聽狀態(tài)
void start()
{
while(true)
{
struct sockaddr_in cli;
socklen_t len=sizeof(cli);
bzero(&cli,len);
int sock=accept(_listensock,(struct sockaddr*)&cli,&len);
if(sock<0)
{
logMessage(FATAL,"accept client error");
continue;
}
logMessage(NORMAL,"accept client success");
cout<<"accept sock: "<<sock<<endl;
}
accept函數(shù)原型
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
sockfd
:是一個已經(jīng)通過socket
函數(shù)創(chuàng)建的套接字描述符,并且是已經(jīng)處于監(jiān)聽狀態(tài),用于監(jiān)聽傳入的連接請求。 -
addr
:是一個指向struct sockaddr
結(jié)構(gòu)的指針,用于接收連接請求的客戶端的地址信息。 -
addrlen
:是一個指向socklen_t
類型的指針,用于指定addr
緩沖區(qū)的長度,同時也用于返回實際客戶端地址結(jié)構(gòu)的大小。 -
accept函數(shù)作用是接受傳入的連接請求,他會阻塞程序的執(zhí)行,直到有一個連接請求到達。一旦有連接請求到達,將會創(chuàng)建一個新的套接字,并返回這個新套接字的文件描述符,這個新套接字用于與客戶端進行通信,同時
addr
和addrlen
會填充上客戶端的地址信息。 -
在服務(wù)器程序中,accept函數(shù)會被用在一個循環(huán)中,以接受多個客戶端的連接請求
start函數(shù)作用是阻塞接受客戶端發(fā)送來的連接請求,使得服務(wù)器與客戶端建立通信
tcpclient.cc
#include<iostream>
#include<string>
#include<memory>
#include"tcpclient.hpp"
using namespace std;
using namespace client;
static void Usage(string proc)
{
cout<<"\nUsage :\n\t"<<proc<<" serverip serverport\n"<<endl;
}
int main(int argc, char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(1);
}
string serverip=argv[1];
uint16_t serverport=atoi(argv[2]);
unique_ptr<tcpclient> tc(new tcpclient(serverip,serverport));
tc->initclient();
tc->start();
return 0;
}
tcpclient.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
using namespace std;
#define NUM 1024
namespace client
{
class tcpclient
{
public:
tcpclient(const string& ip,const uint16_t& port)
:_sock(-1)
,_port(port)
,_ip(ip)
{}
void initclient()
{
//1.創(chuàng)建sockfd
_sock=socket(AF_INET,SOCK_STREAM,0);
if(_sock<0)
{
cerr<<"socket create error"<<endl;
exit(2);
}
//2.綁定 ip port,不顯示綁定,OS自動綁定
}
void start()
{
struct sockaddr_in ser;
bzero(&ser,sizeof(ser));
socklen_t len=sizeof(ser);
ser.sin_family=AF_INET;
ser.sin_port=htons(_port);
ser.sin_addr.s_addr=inet_addr(_ip.c_str());
if(connect(_sock,(struct sockaddr *)&ser,len)!=0)
{
cerr<<"connect error"<<endl;
}else
{
string msg;
while(true)
{
cout<<"Enter# ";
getline(cin,msg);
write(_sock,msg.c_str(),msg.size());
char inbuffer[NUM];
int n=read(_sock,inbuffer,sizeof(inbuffer)-1);
if(n>0)
{
cout<<"server return :"<<inbuffer<<endl;
}else
{
break;
}
}
}
}
~tcpclient()
{
if(_sock>=0) close(_sock);
}
private:
int _sock;
uint16_t _port;
string _ip;
};
}
tcpserver.cc
#include"tcpserver.hpp"
#include"log.hpp"
#include<iostream>
#include<stdlib.h>
#include<memory>
using namespace Server;
using namespace std;
static void Usage(string proc)
{
cout<<"\nUsage:\n\t"<<proc<<" local_port\n\n"<<endl;
}
int main(int argc,char* argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port=atoi(argv[1]);//將字符串轉(zhuǎn)化為整數(shù)
unique_ptr<tcpserver> ts(new tcpserver(port));
ts->initserver();
ts->start();
return 0;
}
tcpserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include"log.hpp"
#define NUM 1024
using namespace std;
namespace Server
{
enum
{
USAGE_ERR=1,SOCK_ERR,BIND_ERR,LISTEN_ERR
};
class tcpserver;
class ThreadData
{
public:
ThreadData( tcpserver* self,int psock):_this(self),_psock(psock){}
tcpserver* _this;
int _psock;
};
class tcpserver
{
public:
tcpserver(const uint16_t& port):_port(port),_listensock(-1){}
void initserver()
{
//1.創(chuàng)建套接字
_listensock=socket(AF_INET,SOCK_STREAM,0);
if(_listensock<0)
{
logMessage(FATAL,"create listensocket error");
exit(SOCK_ERR);
}
logMessage(NORMAL, "create socket success: %d", _listensock);
//2.bind ip和port
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//綁定失敗
{
logMessage(FATAL,"bind error");
exit(BIND_ERR);
}
logMessage(NORMAL,"bind success");
//3.將套接字設(shè)置為監(jiān)聽模式
if(listen(_listensock,0)<0)
{
logMessage(FATAL,"listen error");
exit(LISTEN_ERR);
}
logMessage(NORMAL,"listen success");
}
void start()
{
// signal(SIGCHLD, SIG_IGN);
threadPool<Task>::getthpptr()->run();
while(true)
{
struct sockaddr_in cli;
socklen_t len=sizeof(cli);
bzero(&cli,len);
int sock=accept(_listensock,(struct sockaddr*)&cli,&len);
if(sock<0)
{
logMessage(FATAL,"accept client error");
continue;
}
logMessage(NORMAL,"accept client success");
cout<<"accept sock: "<<sock<<endl;
// serviceIO(sock);//客戶端串行版
// close(sock);
//多進程版---
//一個客戶端占用一個文件描述符,原因在于孫子進程執(zhí)行IO任務(wù)需要占用獨立的文件描述符,而文件描述符是繼承父進程的,而每次客戶端進來都要占用新的文件描述符
//因此若接收多個客戶端不退出的話文件描述符會越來越少。
// pid_t id=fork();//創(chuàng)建子進程
// if(id==0)//子進程進入
// {
// close(_listensock);//子進程不需要用于監(jiān)聽因此關(guān)閉該文件描述符
// if(fork()>0) exit(0);
// //子進程創(chuàng)建孫子進程,子進程直接退出,讓孫子進程擔任IO任務(wù),且孫子進程成為孤兒進程被OS領(lǐng)養(yǎng),
// //除非客戶端退出IO任務(wù)結(jié)束否則該孤兒進程一直運行下去不會相互干擾,即并行完成服務(wù)器和客戶端的通信
// //孫子進程
// serviceIO(sock);
// close(sock);
// exit(0);
// }
// //父進程
// pid_t ret=waitpid(id,nullptr,0);
// if(ret<0)
// {
// cout << "waitsuccess: " << ret << endl;
// }
//多線程版
// pthread_t pid;
// ThreadData* th=new ThreadData(this,sock);
// pthread_create(&pid,nullptr,start_routine,th);
threadPool<Task>::getthpptr()->push(Task(sock,serviceIO));
}
}
// static void* start_routine(void* args)
// {
// pthread_detach(pthread_self());
// ThreadData* ret=static_cast<ThreadData*>(args);
// ret->_this->serviceIO(ret->_psock);
// close(ret->_psock);
// delete ret;
// return nullptr;
// }
// void serviceIO(int sock)
// {
// char inbuffer[NUM];
// while(true)
// {
// ssize_t n=read(sock,inbuffer,sizeof(inbuffer)-1);
// if(n>0)
// {
// inbuffer[n]=0;
// cout<<"recv message: "<<inbuffer<<endl;
// string outb=inbuffer;
// string outbuffer=outb+"[server echo]";
// write(sock,outbuffer.c_str(),outbuffer.size());
// }
// else
// {
// logMessage(NORMAL,"client quit,i quit yep");
// break;
// }
// }
// }
~tcpserver(){}
private:
int _listensock;//用于監(jiān)聽服務(wù)器的sock文件描述符
uint16_t _port;//端口號
};
}
1. 單進程版:客戶端串行版
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include"log.hpp"
#define NUM 1024
using namespace std;
namespace Server
{
enum
{
USAGE_ERR=1,SOCK_ERR,BIND_ERR,LISTEN_ERR
};
class tcpserver
{
public:
tcpserver(const uint16_t& port):_port(port),_listensock(-1){}
void initserver()
{
//1.創(chuàng)建套接字
_listensock=socket(AF_INET,SOCK_STREAM,0);
if(_listensock<0)
{
logMessage(FATAL,"create listensocket error");
exit(SOCK_ERR);
}
logMessage(NORMAL, "create socket success: %d", _listensock);
//2.bind ip和port
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//綁定失敗
{
logMessage(FATAL,"bind error");
exit(BIND_ERR);
}
logMessage(NORMAL,"bind success");
//3.將套接字設(shè)置為監(jiān)聽模式
if(listen(_listensock,0)<0)
{
logMessage(FATAL,"listen error");
exit(LISTEN_ERR);
}
logMessage(NORMAL,"listen success");
}
void start()
{
while(true)
{
struct sockaddr_in cli;
socklen_t len=sizeof(cli);
bzero(&cli,len);
int sock=accept(_listensock,(struct sockaddr*)&cli,&len);
if(sock<0)
{
logMessage(FATAL,"accept client error");
continue;
}
logMessage(NORMAL,"accept client success");
cout<<"accept sock: "<<sock<<endl;
serviceIO(sock);//客戶端串行版
close(sock);
}
}
void serviceIO(int sock)
{
char inbuffer[NUM];
while(true)
{
ssize_t n=read(sock,inbuffer,sizeof(inbuffer)-1);
if(n>0)
{
inbuffer[n]=0;
cout<<"recv message: "<<inbuffer<<endl;
string outb=inbuffer;
string outbuffer=outb+"[server echo]";
write(sock,outbuffer.c_str(),outbuffer.size());
}
else
{
logMessage(NORMAL,"client quit,i quit yep");
break;
}
}
}
~tcpserver(){}
private:
int _listensock;//用于監(jiān)聽服務(wù)器的sock文件描述符
uint16_t _port;//端口號
};
}
注意:客戶端串行給服務(wù)器發(fā)數(shù)據(jù)是在哪里堵塞?由于阻塞在accept函數(shù)處,即accept等待客戶端接入是阻塞式等待。accept函數(shù)接收了一個連接請求后,后來的客戶端連接請求需要在accept函數(shù)處等待,當上一個客戶端退出后,服務(wù)器才能accept當前客戶端發(fā)送來的連接請求成功,才能接收當前客戶端的數(shù)據(jù)。即服務(wù)器串行接收處理客戶端發(fā)送來的數(shù)據(jù)
2. 多進程版:客戶端并行版
tcpserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include"log.hpp"
#define NUM 1024
using namespace std;
namespace Server
{
enum
{
USAGE_ERR=1,SOCK_ERR,BIND_ERR,LISTEN_ERR
};
class tcpserver
{
public:
tcpserver(const uint16_t& port):_port(port),_listensock(-1){}
void initserver()
{
//1.創(chuàng)建套接字
_listensock=socket(AF_INET,SOCK_STREAM,0);
if(_listensock<0)
{
logMessage(FATAL,"create listensocket error");
exit(SOCK_ERR);
}
logMessage(NORMAL, "create socket success: %d", _listensock);
//2.bind ip和port
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//綁定失敗
{
logMessage(FATAL,"bind error");
exit(BIND_ERR);
}
logMessage(NORMAL,"bind success");
//3.將套接字設(shè)置為監(jiān)聽模式
if(listen(_listensock,0)<0)
{
logMessage(FATAL,"listen error");
exit(LISTEN_ERR);
}
logMessage(NORMAL,"listen success");
}
void start()
{
// signal(SIGCHLD, SIG_IGN);
while(true)
{
struct sockaddr_in cli;
socklen_t len=sizeof(cli);
bzero(&cli,len);
int sock=accept(_listensock,(struct sockaddr*)&cli,&len);
if(sock<0)
{
logMessage(FATAL,"accept client error");
continue;
}
logMessage(NORMAL,"accept client success");
cout<<"accept sock: "<<sock<<endl;
//多進程版---
//一個客戶端占用一個文件描述符,原因在于孫子進程執(zhí)行IO任務(wù)需要占用獨立的文件描述符,而文件描述符是繼承父進程的,而每次客戶端進來都要占用新的文件描述符
//因此若接收多個客戶端不退出的話文件描述符會越來越少。
pid_t id=fork();//創(chuàng)建子進程
if(id==0)//子進程進入
{
close(_listensock);//子進程不需要用于監(jiān)聽因此關(guān)閉該文件描述符
if(fork()>0) exit(0);
// //子進程創(chuàng)建孫子進程,子進程直接退出,讓孫子進程擔任IO任務(wù),且孫子進程成為孤兒進程被OS領(lǐng)養(yǎng),
// //除非客戶端退出IO任務(wù)結(jié)束否則該孤兒進程一直運行下去不會相互干擾,即并行完成服務(wù)器和客戶端的通信
// //孫子進程
serviceIO(sock);
close(sock);
exit(0);
}
//父進程
// close(sock);//父進程不使用文件描述符就關(guān)閉
pid_t ret=waitpid(id,nullptr,0);
if(ret<0)
{
cout << "waitsuccess: " << ret << endl;
}
}
}
void serviceIO(int sock)
{
char inbuffer[NUM];
while(true)
{
ssize_t n=read(sock,inbuffer,sizeof(inbuffer)-1);
if(n>0)
{
inbuffer[n]=0;
cout<<"recv message: "<<inbuffer<<endl;
string outb=inbuffer;
string outbuffer=outb+"[server echo]";
write(sock,outbuffer.c_str(),outbuffer.size());
}
else
{
logMessage(NORMAL,"client quit,i quit yep");
break;
}
}
}
~tcpserver(){}
private:
int _listensock;//用于監(jiān)聽服務(wù)器的sock文件描述符
uint16_t _port;//端口號
};
}
-
父進程fork創(chuàng)建子進程,創(chuàng)建完后waitpid等待回收子進程。子進程fork創(chuàng)建孫子進程,創(chuàng)建完后直接退出。導(dǎo)致孫子進程成為孤兒進程,進而被OS領(lǐng)養(yǎng)。因此除非客戶端退出IO任務(wù),否則孤兒進程將一直運行下去不會干擾到其他進程,即并行完成服務(wù)器和客戶端的通信
-
注意的是服務(wù)器accept一次客戶端的連接請求,就需要申請一個文件描述符,而文件描述符是有上限的,如果大量的客戶端請求連接成功并且不結(jié)束的話,會造成文件描述符泄露。
因此在父進程那里需要關(guān)閉不使用的文件描述符
- 父進程這里回收子進程,不能使用非阻塞等待,原因在于非阻塞等待的本質(zhì)是輪詢,而這里使用后會導(dǎo)致父進程會在accept函數(shù)處阻塞等待客戶端發(fā)送連接請求,那么父進程就無法回收子進程了。因此waitpid的返回值用ret接收,等待回收成功就打印日志,失敗則跳過
- 當子進程圖退出或者被中止時子進程會發(fā)送17號信號SIGCHILD給父進程,父進程可以通過忽略17號信號SIGCHILD的方式來不阻塞等待回收子進程(這種方法對于linux環(huán)境可用,其余不保證)
signal(SIGCHLD, SIG_IGN);
netstat查看網(wǎng)絡(luò)信息
netstat
是一個用于查看網(wǎng)絡(luò)連接和網(wǎng)絡(luò)統(tǒng)計信息的命令行工具。它可以用來顯示當前系統(tǒng)上的網(wǎng)絡(luò)連接、路由表、接口統(tǒng)計信息等等。在 Linux 系統(tǒng)中,netstat
命令的用法如下:
netstat [options]
一些常用的選項包括:
-
-a
:顯示所有的連接,包括監(jiān)聽中和已建立的連接。 -
-t
:顯示 TCP 協(xié)議的連接。 -
-u
:顯示 UDP 協(xié)議的連接。 -
-n
:以數(shù)字形式顯示 IP 地址和端口號,而不是嘗試進行 DNS 解析。 -
-p
:顯示與連接關(guān)聯(lián)的進程信息。 -
-r
:顯示路由表。 -
-l
:僅顯示監(jiān)聽中的連接。 -
-atun
:顯示所有的TCP和UDP連接
注意一下:這里出現(xiàn)了兩個連接,原因在于服務(wù)器和客戶端在同一臺主機上,即服務(wù)器和客戶端完成了本地環(huán)回,因此能看到兩個連接。
3.多線程版:并行執(zhí)行
tcpserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include"log.hpp"
#define NUM 1024
using namespace std;
namespace Server
{
enum
{
USAGE_ERR=1,SOCK_ERR,BIND_ERR,LISTEN_ERR
};
class tcpserver;
class ThreadData
{
public:
ThreadData( tcpserver* self,int psock):_this(self),_psock(psock){}
tcpserver* _this;
int _psock;
};
class tcpserver
{
public:
tcpserver(const uint16_t& port):_port(port),_listensock(-1){}
void initserver()
{
//1.創(chuàng)建套接字
_listensock=socket(AF_INET,SOCK_STREAM,0);
if(_listensock<0)
{
logMessage(FATAL,"create listensocket error");
exit(SOCK_ERR);
}
logMessage(NORMAL, "create socket success: %d", _listensock);
//2.bind ip和port
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//綁定失敗
{
logMessage(FATAL,"bind error");
exit(BIND_ERR);
}
logMessage(NORMAL,"bind success");
//3.將套接字設(shè)置為監(jiān)聽模式
if(listen(_listensock,0)<0)
{
logMessage(FATAL,"listen error");
exit(LISTEN_ERR);
}
logMessage(NORMAL,"listen success");
}
void start()
{
while(true)
{
struct sockaddr_in cli;
socklen_t len=sizeof(cli);
bzero(&cli,len);
int sock=accept(_listensock,(struct sockaddr*)&cli,&len);
if(sock<0)
{
logMessage(FATAL,"accept client error");
continue;
}
logMessage(NORMAL,"accept client success");
cout<<"accept sock: "<<sock<<endl;
//多線程版
pthread_t pid;
ThreadData* th=new ThreadData(this,sock);
pthread_create(&pid,nullptr,start_routine,th);
}
}
static void* start_routine(void* args)
{
pthread_detach(pthread_self());//線程分離后讓OS自動回收新線程
ThreadData* ret=static_cast<ThreadData*>(args);
ret->_this->serviceIO(ret->_psock);
close(ret->_psock);
delete ret;
return nullptr;
}
void serviceIO(int sock)
{
char inbuffer[NUM];
while(true)
{
ssize_t n=read(sock,inbuffer,sizeof(inbuffer)-1);
if(n>0)
{
inbuffer[n]=0;
cout<<"recv message: "<<inbuffer<<endl;
string outb=inbuffer;
string outbuffer=outb+"[server echo]";
write(sock,outbuffer.c_str(),outbuffer.size());
}
else
{
logMessage(NORMAL,"client quit,i quit yep");
break;
}
}
}
~tcpserver(){}
private:
int _listensock;//用于監(jiān)聽服務(wù)器的sock文件描述符
uint16_t _port;//端口號
};
}
- 服務(wù)器接收一個客戶端的連接請求,就申請一個新線程,多線程下可以讓服務(wù)器接收多個線程
log.hpp
#pragma once
#include <iostream>
#include <string>
#include<ctime>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdarg.h>
using namespace std;
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
#define NUM 1024
#define LOG_STR "./logstr.txt"
#define LOG_ERR "./log.err"
const char* to_str(int level)
{
switch(level)
{
case DEBUG: return "DEBUG";
case NORMAL: return "NORMAL";
case WARNING: return "WARNING";
case ERROR: return "ERROR";
case FATAL: return "FATAL";
default: return nullptr;
}
}
void logMessage(int level, const char* format,...)
{
// [日志等級] [時間戳/時間] [pid] [messge]
// [WARNING] [2023-05-11 18:09:08] [123] [創(chuàng)建socket失敗]
// 暫定
// std::cout << message << std::endl;
char logprestr[NUM];
snprintf(logprestr,sizeof(logprestr),"[%s][%ld][%d]",to_str(level),(long int)time(nullptr),getpid());//把后面的內(nèi)容打印進logprestr緩存區(qū)中
char logeldstr[NUM];
va_list arg;
va_start(arg,format);
vsnprintf(logeldstr,sizeof(logeldstr),format,arg);//arg是logmessage函數(shù)列表中的...
cout<<logprestr<<logeldstr<<endl;
// FILE* str=fopen(LOG_STR,"a");
// FILE* err=fopen(LOG_ERR,"a");//以追加方式打開文件,若文件不存在則創(chuàng)建
// if(str!=nullptr||err!=nullptr)//兩個文件指針都不為空則創(chuàng)建文件成功
// {
// FILE* ptr=nullptr;
// if(level==DEBUG||level==NORMAL||level==WARNING)
// {
// ptr=str;
// }
// if(level==ERROR||level==FATAL)
// {
// ptr=err;
// }
// if(ptr!=nullptr)
// {
// fprintf(ptr,"%s-%s\n",logprestr,logeldstr);
// }
// fclose(str);
// fclose(err);
//}
}
可變參數(shù)列表
va_list是(char*)重命名的類型,定義可以訪問可變參數(shù)的變量。
va_start(ap, v) ap是定義的可變參數(shù)變量,v是形參中可變參數(shù)前第一個參數(shù)名,其作用是使ap指向可變參數(shù)部分。
va_arg(ap, t) ap是定義的可變參數(shù)變量,t是可變參數(shù)的類型,根據(jù)類型,訪問可變參數(shù)列表中的數(shù)據(jù)。
va_end(ap) ap是定義的可變參數(shù)變量,使ap變量置空,作為結(jié)束使用。
vsnprintf函數(shù)原型
#include <stdio.h>
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
-
str是一個指向字符數(shù)組(緩沖區(qū))的指針,用于存儲格式化后的數(shù)據(jù)
-
size是緩沖區(qū)的大小,限制了寫入的最大字符數(shù),包括終止的 null 字符
-
format格式化字符串,類似于
printf
函數(shù)中的格式化字符串 -
ap是一個
va_list
類型的變量,用于存儲可變參數(shù)列表的信息,并且要注意OS對參數(shù)壓棧的順序是從右向左 -
vsnprintf
函數(shù)根據(jù)指定的format
格式化字符串將數(shù)據(jù)寫入str
緩沖區(qū),但不會超出指定的緩沖區(qū)大小。它會在寫入數(shù)據(jù)后自動在緩沖區(qū)末尾添加一個 null 終止字符,確保結(jié)果是一個合法的 C 字符串。
守護進程
守護進程(Daemon)是在計算機系統(tǒng)中以后臺方式運行的一類特殊進程。它通常在操作系統(tǒng)啟動時被初始化,并在整個系統(tǒng)運行期間保持活動狀態(tài),不需要與用戶交互。守護進程通常用于執(zhí)行系統(tǒng)任務(wù)、服務(wù)管理以及提供后臺服務(wù),如網(wǎng)絡(luò)服務(wù)、定時任務(wù)等。
守護進程特點如下:
- 后臺運行,守護進程在后臺運行,不與用戶交互,沒有控制終端。
- 獨立性:它通常獨立于用戶會話,即使用戶注銷或關(guān)閉終端,守護進程也會繼續(xù)運行。
- 沒有標準輸入輸出:守護進程通常沒有標準輸入和輸出,因為它們不與用戶交互。它們通常將輸出寫入日志文件。
- 分離自身:守護進程會通過一系列操作來與終端、會話和控制組脫離連接,以確保它不會意外地被控制終端關(guān)閉。
一個服務(wù)器中可以具有多個會話,例如一個服務(wù)器上有一個root用戶和多個普通用戶,當普通用戶登錄上服務(wù)器時即成為一個會話。
一個會話具有多個后臺任務(wù),但只能具有一個前臺任務(wù)(bash)。
- jobs查看任務(wù)可以看到任務(wù)1是./tcpserver,任務(wù)2是sleep 1000 | sleep 2000 | sleep 3000 &,任務(wù)3是sleep 4000 | sleep 5000 &,且三個任務(wù)后面都帶&,在進程或任務(wù)后帶&作用是將該任務(wù)放到后臺運行
- sleep 1000 、sleep 2000 、sleep 3000 、sleep 4000、sleep 5000的父進程都是16853即bash;而 sleep 1000 、sleep 2000 、sleep 3000的PGID相同,都是sleep 1000的pid,即 sleep 1000 、sleep 2000 、sleep 3000屬于同一組,同一個組要協(xié)同起來完成同一個作業(yè)。第一個任務(wù)的pid是組長的pid即sleep 1000的pid;而小組16858和小組17070的SID都是16853,即這兩個小組屬于同一個會話(bash),要完成的是同一個任務(wù);
fg、bg
fg 作業(yè)號:將作業(yè)放到前臺
bg 作業(yè)號:將作業(yè)放到后臺,或者繼續(xù)執(zhí)行后臺作業(yè)
ctrl+Z將前臺任務(wù)暫停并把作業(yè)放到后臺
- 用戶登錄時服務(wù)器就需要為此創(chuàng)建一些后臺作業(yè)和前臺作業(yè)(命令行)來服務(wù)用戶,而用戶注銷或退出服務(wù)器也會影響其前臺作業(yè)和后臺作業(yè)。而服務(wù)器程序不能受到用戶登錄和注銷的影響。
- 我們可以使得服務(wù)器程序自成會話,自成進程組,那么該程序就與終端設(shè)備無關(guān),不能再收到用戶登錄和注銷的影響了。該類進程被稱為守護進程
setsid
在Unix和類Unix系統(tǒng)中,
setsid
是一個用于創(chuàng)建新會話的系統(tǒng)調(diào)用函數(shù)。會話(Session)是一組相關(guān)的進程組合,通常由一個控制終端和一些子進程組成。setsid
函數(shù)的主要作用是將調(diào)用它的進程從當前會話中分離出來,并創(chuàng)建一個新的會話。
#include <unistd.h>
pid_t setsid(void);
-
創(chuàng)建新會話:調(diào)用
setsid
的進程會成為一個新的會話的組長(Session Leader)。新會話不再與之前的控制終端相關(guān)聯(lián)。但該進程在調(diào)用setsid函數(shù)之前不能是組長。 -
分離終端:調(diào)用
setsid
的進程不再與任何控制終端關(guān)聯(lián),無法重新獲得控制終端。 -
成為新進程組的組長:新會話中的第一個進程(調(diào)用
setsid
的進程)會成為新的進程組的組長。
daemon.hpp
#pragma once
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define DEV "/dev/null"
void daemonSelf(const char *currPath = nullptr)
{
// 1. 讓調(diào)用進程忽略掉異常的信號
signal(SIGPIPE,SIG_IGN);//選擇忽略SIGPIPE信號
// 2. 如何讓自己不是組長,setsid
if(fork()>0)
exit(0);//父進程退出
// 子進程 -- 守護進程,精靈進程,本質(zhì)就是孤兒進程的一種!
pid_t ret=setsid();
assert(ret!=-1);
// 3. 守護進程是脫離終端的,關(guān)閉或者重定向以前進程默認打開的文件
int fd=open(DEV,O_RDWR);
if(fd>=0)
{
//dup2(oldfd,newfd):將oldfd的內(nèi)容填充到newfd中,這樣輸入到newfd的內(nèi)容被重定向到oldfd
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
}else
{
close(0);
close(1);
close(2);
}
// 4. 可選:進程執(zhí)行路徑發(fā)生更改
if(currPath) chdir(currPath);//更改currPath的路徑
}
-
/dev/null
是一個特殊的設(shè)備文件,它被用作數(shù)據(jù)丟棄點,向它寫入的數(shù)據(jù)會被丟棄,從它讀取數(shù)據(jù)會立即返回EOF(End of File) - SIGPIPE的觸發(fā)場景:當一個進程向一個已經(jīng)關(guān)閉寫端的管道(或者套接字)寫數(shù)據(jù)時、當進程向一個已經(jīng)收到
RST
包(連接重置)的套接字發(fā)送數(shù)據(jù)時,該進程就會向父進程發(fā)送SIGPIPE信號來進行進程終止。對SIGPIPE進行忽略行為避免了進程向/dev/null中寫入數(shù)據(jù)并出現(xiàn)錯誤導(dǎo)致的進程終止 - 父進程創(chuàng)建子進程,父進程作為組長,父進程退出后,子進程能夠自己成為組長即能夠成為守護進程
- dup2(oldfd,newfd):將oldfd的內(nèi)容填充到newfd中,這樣輸入到newfd的內(nèi)容被重定向到oldfd。在代碼中是將輸入文件描述符012的內(nèi)容重定向到fd即/dev/null中
tcpserver.cc
#include"tcpserver.hpp"
#include"log.hpp"
#include"daemon.hpp"
#include<iostream>
#include<stdlib.h>
#include<memory>
using namespace Server;
using namespace std;
static void Usage(string proc)
{
cout<<"\nUsage:\n\t"<<proc<<" local_port\n\n"<<endl;
}
int main(int argc,char* argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port=atoi(argv[1]);//將字符串轉(zhuǎn)化為整數(shù)
unique_ptr<tcpserver> ts(new tcpserver(port));
ts->initserver();
daemonSelf();
ts->start();
return 0;
}
TCP協(xié)議通信流程
首先需要服務(wù)器初始化
服務(wù)器初始化:
- 調(diào)用socket, 創(chuàng)建文件描述符,該文件描述符用于監(jiān)聽;
- 調(diào)用bind, 將當前的文件描述符和ip/port綁定在一起; 如果這個端口已經(jīng)被其他進程占用了, 就會bind失敗;
- 調(diào)用listen, 聲明當前這個文件描述符作為一個服務(wù)器的文件描述符, 該文件描述符處于監(jiān)聽狀態(tài),等待客戶端發(fā)起連接;
- 調(diào)用accecpt, 并阻塞, 等待客戶端連接過來;
建立連接的過程(通常稱為三次握手)
建立連接的過程(內(nèi)含三次握手)
- 客戶端調(diào)用socket,創(chuàng)建文件描述符;
- 客戶端調(diào)用connect,向指定地址端口的服務(wù)器發(fā)起請求;(請求的過程中會進行三次握手)
- connect會發(fā)出SYN段給服務(wù)器并阻塞等待服務(wù)器應(yīng)答;(第一次握手)
- 服務(wù)器收到客戶端的SYN段后,會應(yīng)答一個SYN-ACK段表示"同意建立連接";(第二次握手)
- 客戶端收到SYN-ACK后,會從connet()返回,同時發(fā)送一個應(yīng)答ACK段給服務(wù)器;(第三次握手)
- 服務(wù)器收到客戶端發(fā)來的ACK段后,會從accpet()返回,返回(分配)一個新的文件描述符connfd用于與客戶端通信
- 可以看到三次握手由connect()發(fā)起請求開始,并由connect()返回結(jié)束,因此客戶端在調(diào)用connect()時本質(zhì)就是通過某種方式與服務(wù)器進行三次握手
**對于建鏈接的3次握手,**主要是要初始化Sequence Number 的初始值。通信的雙方要互相通知對方自己的初始化的Sequence Number(縮寫為ISN:Inital Sequence Number)——所以叫SYN,全稱Synchronize Sequence Numbers。也就上圖中的 x 和 y。這個號要作為以后的數(shù)據(jù)通信的序號,以保證應(yīng)用層接收到的數(shù)據(jù)不會因為網(wǎng)絡(luò)上的傳輸?shù)膯栴}而亂序(TCP會用這個序號來拼接數(shù)據(jù))。來自陳浩大佬對于三次握手的部分詮釋
- 連接建立成功后會被accpet獲取到,此時客戶端和服務(wù)器就能進行通信了。要注意的是,連接建立是三次握手做的事,三次握手是TCP底層的工作,而accep要做的是把底層已經(jīng)建立好的連接拿到用戶層,即accept本身不參于三次握手這個過程(不參與建立連接),accpet會阻塞等待獲取建立好的連接,若連接沒有建立好會進行等待。
數(shù)據(jù)傳輸?shù)倪^程
建立連接后,TCP協(xié)議提供全雙工的通信服務(wù); 所謂全雙工的意思是, 在同一條連接中, 同一時刻, 通信雙方 可以同時寫數(shù)據(jù); 其原因在于服務(wù)器和客戶端的應(yīng)用層和傳輸層都有兩個緩沖區(qū),一個是發(fā)送緩沖區(qū)另一個是接收緩沖區(qū),那么服務(wù)器和客戶端進行發(fā)送和讀取并不會互相影響。相對的概念叫做半雙工, 同一條連接在同一時刻, 只能由一方來寫數(shù)據(jù);
服務(wù)器從accept()返回后立刻調(diào) 用read(), 讀socket就像讀管道一樣, 如果沒有數(shù)據(jù)到達就阻塞等待;
這時客戶端調(diào)用write()發(fā)送請求給服務(wù)器, 服務(wù)器收到后從read()返回,對客戶端的請求進行處理, 在此期 間客戶端調(diào)用read()阻塞等待服務(wù)器的應(yīng)答;
服務(wù)器調(diào)用write()將處理結(jié)果發(fā)回給客戶端, 再次調(diào)用read()阻塞等待下一條請求;
客戶端收到后從read()返回, 發(fā)送下一條請求,如此循環(huán)下去
斷開連接的過程
- 如果客戶端沒有更多的請求了, 就調(diào)用close()關(guān)閉連接, 客戶端會向服務(wù)器發(fā)送FIN段;(第一次握手)
- 此時服務(wù)器收到FIN后, 會回應(yīng)一個ACK, 同時read會返回0 ;(第二次握手)
- read返回之后, 服務(wù)器就知道客戶端關(guān)閉了連接, 也調(diào)用close關(guān)閉連接, 這個時候服務(wù)器會向客戶端發(fā)送 一個FIN; (第三次握手)
- 客戶端收到FIN, 再返回一個ACK給服務(wù)器; (第四次握手)
-
這個斷開連接的過程, 通常稱為四次揮手
-
對于4次揮手其實你仔細看是2次,因為TCP是全雙工的,所以,發(fā)送方和接收方都需要Fin和Ack。只不過,有一方是被動的,所以看上去就成了所謂的4次揮手。如果兩邊同時斷連接,那就會就進入到CLOSING狀態(tài),然后到達TIME_WAIT狀態(tài)。文章來源:http://www.zghlxwxcb.cn/news/detail-683136.html
當客戶端不與服務(wù)器通信時需要斷開連接的原因文章來源地址http://www.zghlxwxcb.cn/news/detail-683136.html
- 其實,網(wǎng)絡(luò)上的傳輸是沒有連接的,包括TCP也是一樣的。而TCP所謂的“連接”,其實只不過是在通訊的雙方維護一個“連接狀態(tài)”,讓它看上去好像有連接一樣。所以,TCP的狀態(tài)變換是非常重要的。若通信結(jié)束不及時斷開連接,即占用著操作系統(tǒng)的資源不使用,會導(dǎo)致系統(tǒng)的資源越來越少。
- 服務(wù)器能夠與多個客戶端建立連接,意味著服務(wù)器會收到大量的連接,因此操作系統(tǒng)要對這些連接進行管理,即"先組織再管理",在服務(wù)端就需要維護連接相關(guān)的數(shù)據(jù)結(jié)構(gòu),把這些數(shù)據(jù)結(jié)構(gòu)組織起來,那么對連接的管理轉(zhuǎn)變?yōu)閷?shù)據(jù)結(jié)構(gòu)的管理。
- 操作系統(tǒng)需要維護這些連接相關(guān)的數(shù)據(jù)結(jié)構(gòu),勢必需要消耗資源,而不通信的連接不斷開,會導(dǎo)致操作系統(tǒng)的資源浪費。而TCP與UDP的區(qū)別之一在于TCP需要對連接相關(guān)的資源進行管理。
到了這里,關(guān)于網(wǎng)絡(luò)字節(jié)序——TCP接口及其實現(xiàn)簡單TCP服務(wù)器的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!