一、TCP通信簡(jiǎn)單模擬實(shí)現(xiàn)
Tcp通信模擬實(shí)現(xiàn)與Udp通信模擬實(shí)現(xiàn)的區(qū)別不大,一個(gè)是面向字節(jié)流,一個(gè)是面向數(shù)據(jù)報(bào);udp協(xié)議下拿到的數(shù)據(jù)可以直接發(fā)送,tcp協(xié)議下需要?jiǎng)?chuàng)建鏈接,用文件描述符完成數(shù)據(jù)的讀寫
1.1 服務(wù)端實(shí)現(xiàn)
1.1.1 接口認(rèn)識(shí)
1.1.1.1 listen:監(jiān)聽socket
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd:創(chuàng)建的套接字
backlog:新連接隊(duì)列的長(zhǎng)度限制
1.1.1.2 accept:獲取連接
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:創(chuàng)健的套接字,僅用于監(jiān)聽新鏈接
addr:結(jié)構(gòu)體,這里說(shuō)網(wǎng)絡(luò)通信,用sockaddr_in
addrlen:結(jié)構(gòu)體大小
返回值:一個(gè)新的文件描述符(套接字),這個(gè)才是和客戶端通信的文件描述符
通信就用accept返回的文件描述符,面向字節(jié)流,后續(xù)都是文件操作
1.1.2 tcpServer.hpp
#pragma once
#include "logMessage.hpp"
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <unistd.h>
namespace Server
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
using namespace std;
static const uint16_t gport = 8080;
static const uint16_t gbacklog = 5;
class tcpServer
{
public:
tcpServer(const uint16_t &port = gport)
: listen_sockfd_(-1), port_(port)
{
}
void InitServer()
{
// 1.創(chuàng)建socket
listen_sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sockfd_ < 0)
{
logMessage(FATAL, "create socket error");
exit(SOCKET_ERR);
}
logMessage(NORMAL, "create socket success");
// 2.bind網(wǎng)絡(luò)信息
struct sockaddr_in local;
memset(&local,0,sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons(port_);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(listen_sockfd_,(struct sockaddr*)&local,sizeof(local))<0)
{
logMessage(FATAL,"bind socket error");
exit(BIND_ERR);
}
logMessage(NORMAL,"bind socket success");
//3.設(shè)置socket為監(jiān)聽狀態(tài)
if(listen(listen_sockfd_,gbacklog)<0)
{
logMessage(FATAL,"listen socket error");
exit(LISTEN_ERR);
}
logMessage(NORMAL,"listen socket success");
}
void start()
{
for(; ;)
{
//4.server獲取新鏈接
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
//sock:和客戶端通信的文件描述符
int sock=accept(listen_sockfd_,(struct sockaddr*)&peer,&len);
if(sock<0)//沒有獲取新鏈接成功就執(zhí)行下一次循環(huán)
{
logMessage(FATAL,"accpect sock error");
continue;
}
logMessage(NORMAL,"accept sock success");
std::cout<<"sock"<<sock<<endl;
//5.通信就用sock文件描述符,面向字節(jié)流,后續(xù)都是文件操作
/*version1*/
serverIO(sock);
close(sock);
}
}
void serverIO(int sock)
{
char buffer[1024];
while (true)
{
ssize_t n=read(sock,buffer,sizeof(buffer)-1);
if(n>0)
{
buffer[n]=0;
std::cout<<"recv message "<<buffer<<endl;
string outbuffer=buffer;
outbuffer+="server[echo]";
write(sock,outbuffer.c_str(),outbuffer.size());
}
else if(n==0)
{
//客戶端退出
logMessage(NORMAL,"client quit ,me too!!");
break;
}
}
}
~tcpServer()
{
}
private:
int listen_sockfd_;//不負(fù)責(zé)通信,只負(fù)責(zé)監(jiān)聽鏈接,獲取新鏈接
uint16_t port_;
};
}
1.1.3 tcpServer.cc
#include "tcpServer.hpp"
#include <memory>
using namespace std;
using namespace Server;
static void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}
int main(int argc,char* argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t serverport=atoi(argv[1]);
unique_ptr<tcpServer> tsvr(new tcpServer(serverport));
tsvr->InitServer();
tsvr->start();
return 0;
}
1.2 客戶端實(shí)現(xiàn)
1.2.1 接口認(rèn)識(shí)
1.2.1.1 connect:發(fā)起連接
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
參數(shù)與accept一樣,代表的含義也一樣
返回值:成功0,失敗-1
1.2.2 tcpClient.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <string>
#include <unistd.h>
namespace Client
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
using namespace std;
class tcpClient
{
public:
tcpClient(const string &clientip, const uint16_t &clientport)
: clientip_(clientip), clientport_(clientport), sockfd_(-1)
{
}
void InitCient()
{
// 1.創(chuàng)建socket
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0)
{
std::cerr << "socket create error" << endl;
exit(2);
}
}
void run()
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(clientport_);
server.sin_addr.s_addr = inet_addr(clientip_.c_str());
// 發(fā)起鏈接
if (connect(sockfd_, (struct sockaddr *)&server, sizeof(server)) != 0)
{
std::cerr << "connect create error" << endl;
}
else
{
string msg;
while (true)
{
cout << "Enter# ";
getline(cin, msg);
write(sockfd_, msg.c_str(), msg.size());
char buffer [1024];
ssize_t n=read(sockfd_,buffer,sizeof(buffer)-1);
if (n>0)
{
buffer[n]=0;
cout<<"Server處理后為# "<<buffer<<endl;
}
else
{
break;
}
}
}
}
~tcpClient() {
if(sockfd_>=0) close(sockfd_);
}
private:
int sockfd_;
uint16_t clientport_;
string clientip_;
};
}
1.2.3 tcpClient.cc
#include "tcpClient.hpp"
#include <memory>
using namespace std;
using namespace Client;
static void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " server_ip server_port\n\n";
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t clientport=atoi(argv[2]);
string clientip=argv[1];
unique_ptr<tcpClient> ucli(new tcpClient(clientip,clientport));
ucli->InitCient();
ucli->run();
return 0;
}
上述代碼是一個(gè)單進(jìn)程的版本,一個(gè)鏈接過(guò)來(lái)會(huì)去死循環(huán)執(zhí)行serverIO,也就是說(shuō)同一時(shí)間只能有一個(gè)鏈接過(guò)來(lái)通信,其他的鏈接必須阻塞等待上一個(gè)鏈接退出
1.3 優(yōu)化方案
1.3.1 TCP網(wǎng)絡(luò)通信----多進(jìn)程版
更該tcpServer.hpp中的start函數(shù)即可,其他文件和單進(jìn)程版一致
void start()
{
logMessage(NORMAL, "Thread init success");
for (;;)
{
// 4.server獲取新鏈接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// sock:和客戶端通信的文件描述符
int sock = accept(listen_sockfd_, (struct sockaddr *)&peer, &len);
if (sock < 0) // 沒有獲取新鏈接成功就執(zhí)行下一次循環(huán)
{
logMessage(FATAL, "accpect sock error");
continue;
}
logMessage(NORMAL, "accept sock success,get new sock:%d", sock);
/*******************************************version2多進(jìn)程版*/
pid_t id = fork();
if (id == 0) // 子進(jìn)程
{
// 關(guān)閉子進(jìn)程不需要的文件描述符
close(listen_sockfd_);
// 子進(jìn)程退出,父進(jìn)程回收資源,孫子進(jìn)程去執(zhí)行任務(wù)
// 孫子進(jìn)程成為孤兒進(jìn)程,1號(hào)進(jìn)程托管并回收其退出資源
if (fork() > 0)
exit(0);
// 孫子進(jìn)程
serverIO(sock);
close(sock);//任務(wù)完成關(guān)閉文件描述符
exit(0);
}
// 細(xì)節(jié):父進(jìn)程必須關(guān)閉子進(jìn)程的sock,避免一直被占用
// 這里的關(guān)閉并不是完全關(guān)閉,只是引用計(jì)數(shù)減一,并不影響孫子進(jìn)程
close(sock); // 獲取之后立馬關(guān)閉,多次鏈接出現(xiàn)sock都一樣,也可能不一樣
// 父進(jìn)程,阻塞等待子進(jìn)程退出
pid_t ret = waitpid(id, nullptr, 0);
if (ret > 0)
{
std::cout << "waitsuccess" << ret << endl;
}
}
}
- 父進(jìn)程必須關(guān)閉子進(jìn)程的sock,避免一直被占用
這里的關(guān)閉并不是完全關(guān)閉,只是引用計(jì)數(shù)減一,并不影響孫子進(jìn)程- waitpid這里不能單純用非阻塞等待,當(dāng)有多個(gè)連接到來(lái)的時(shí)候,并且有一個(gè)進(jìn)程退出,父進(jìn)程非阻塞等待,去執(zhí)行accept,但是如果后續(xù)沒有連接來(lái)了,就一直阻塞在accept,剩下的子進(jìn)程就沒法回收了
前面【信號(hào)】中曾說(shuō)道子進(jìn)程退出時(shí)會(huì)發(fā)送SIGCHLD信號(hào),我們可以對(duì)其設(shè)置捕捉,忽略掉其行為,父進(jìn)程就不需要阻塞等待了
void start()
{
logMessage(NORMAL, "Thread init success");
signal(SIGCHLD,SIG_IGN);//設(shè)置信號(hào)忽略行為
for (;;)
{
// 4.server獲取新鏈接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// sock:和客戶端通信的文件描述符
int sock = accept(listen_sockfd_, (struct sockaddr *)&peer, &len);
if (sock < 0) // 沒有獲取新鏈接成功就執(zhí)行下一次循環(huán)
{
logMessage(FATAL, "accpect sock error");
continue;
}
logMessage(NORMAL, "accept sock success,get new sock:%d", sock);
/*******************************************version2多進(jìn)程版*/
pid_t id = fork();
if (id == 0) // 子進(jìn)程
{
// 關(guān)閉子進(jìn)程不需要的文件描述符
close(listen_sockfd_);
// 子進(jìn)程
serverIO(sock);
close(sock);//任務(wù)完成關(guān)閉文件描述符
exit(0);
}
// 細(xì)節(jié):父進(jìn)程必須關(guān)閉子進(jìn)程的sock,避免一直被占用
// 這里的關(guān)閉并不是完全關(guān)閉,只是引用計(jì)數(shù)減一,并不影響孫子進(jìn)程
close(sock); // 獲取之后立馬關(guān)閉,多次鏈接出現(xiàn)sock都一樣,也可能不一樣
}
}
1.3.2 TCP網(wǎng)絡(luò)通信----多線程版
在tcpServer類外添加ThreadData類,類內(nèi)修改start函數(shù),添加threadRoutinue函數(shù)其余不變
class tcpServer;
class ThreadData
{
public:
ThreadData(tcpServer *self, int sockfd)
: self_(self), sockfd_(sockfd)
{
}
public:
tcpServer *self_;
int sockfd_;
};
void start()
{
logMessage(NORMAL, "Thread init success");
// signal(SIGCHLD,SIG_IGN);
for (;;)
{
// 4.server獲取新鏈接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// sock:和客戶端通信的文件描述符
int sock = accept(listen_sockfd_, (struct sockaddr *)&peer, &len);
if (sock < 0) // 沒有獲取新鏈接成功就執(zhí)行下一次循環(huán)
{
logMessage(FATAL, "accpect sock error");
continue;
}
logMessage(NORMAL, "accept sock success,get new sock:%d", sock);
/*******************************************version3:多線程版本*/
pthread_t tid;
ThreadData* td=new ThreadData(this,sock);
pthread_create(&tid,nullptr,threadRoutinue,td);
}
}
//類內(nèi)調(diào)用,靜態(tài)方法
static void* threadRoutinue(void* args)
{
pthread_detach(pthread_self());
ThreadData* td= static_cast<ThreadData*>(args);
td->self_->serverIO(td->sockfd_);
//在一個(gè)進(jìn)程中的所有線程都可以訪問到文件描述符表,屬于共享資源,
//一個(gè)線程所對(duì)應(yīng)的fd在使用完畢后需要進(jìn)行關(guān)閉。
close(td->sockfd_);
delete td;
return nullptr;
}
多進(jìn)程版,多線程版,線程池版,可參考我的Gitee
二、日志函數(shù)編寫
在計(jì)算機(jī)中,日志文件是記錄在操作系統(tǒng)或其他軟件運(yùn)行中發(fā)生的事件或在通信軟件的不同用戶之間的消息的文件。記錄是保持日志的行為。在最簡(jiǎn)單的情況下,消息被寫入單個(gè)日志文件。
我們借助可變參數(shù)列表來(lái)模擬實(shí)現(xiàn)日志函數(shù)
實(shí)現(xiàn)格式如:[日志等級(jí)][時(shí)間][pid][message]
#pragma once
#include <iostream>
#include <string>
#include <stdarg.h>
#include <ctime>
#include <unistd.h>
//把錯(cuò)誤信息寫到指定文件
#define LOG_NORMAL "log_nrl.txt"
#define LOG_ERR "log_err.txt"
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
//typedef char* va_list
const char * to_levelstr(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(DEBUG,"sss %f %d %c",2.1,6,'h')
void logMessage(int level,const char* format,...)
{
#define NUM 1024
char logprefix[NUM];
//前半部分[日志等級(jí)][時(shí)間][pid]
snprintf(logprefix,sizeof(logprefix),"[%s][%ld][%d]",to_levelstr(level),(long int)time(nullptr),getpid());
char logcontent[NUM];
va_list arg;
//初始化arg為參數(shù)列表中的第一個(gè)參數(shù)的地址
va_start(arg,format);
//后半部分,錯(cuò)誤信息
//vsnprintf()函數(shù)的作用是將可變參數(shù)列表arg中的數(shù)據(jù)按照指定的格式format寫入緩沖區(qū)logcontent中
vsnprintf(logcontent,sizeof(logcontent),format,arg);
//這里做了簡(jiǎn)化,實(shí)際上是一個(gè)等級(jí)一個(gè)日志文件
FILE* log=fopen(LOG_NORMAL,"a");
FILE* err=fopen(LOG_ERR,"a");
if(log !=nullptr && err!=nullptr)
{
if(level==DEBUG || level==NORMAL||level==WARNING)
{
fprintf(log,"%s%s\n",logprefix,logcontent);
}
if(level==ERROR || level==FATAL)
{
fprintf(err,"%s%s\n",logprefix,logcontent);
}
fclose(log);
fclose(err);
}
}
三、守護(hù)進(jìn)程
3.1 引入:為什么需要守護(hù)進(jìn)程化
守護(hù)進(jìn)程又叫精靈進(jìn)程—本質(zhì)孤兒進(jìn)程的一種有了守護(hù)進(jìn)程,上述的服務(wù)端才能變成一個(gè)真正的服務(wù)端
1.當(dāng)我們使用xsell鏈接遠(yuǎn)端云服務(wù)器的時(shí)候,打開的頁(yè)面第一個(gè)出現(xiàn)的就是bash命令行,這個(gè)時(shí)候我們輸入sleep 10000 | sleep 20000 |sleep 30000 &
就可以添加一個(gè)后臺(tái)任務(wù)。
2.在命令行運(yùn)行sleep 40000 | sleep 50000 |sleep 60000 &
后,查看進(jìn)程,發(fā)現(xiàn)新創(chuàng)建的三個(gè)進(jìn)程PGID一樣,屬于同一個(gè)組,完成一個(gè)任務(wù),與之前的任務(wù)同屬于一個(gè)會(huì)話(SID都一樣)
3.通過(guò)查看SID進(jìn)程發(fā)現(xiàn),是bash:會(huì)話ID是以bash命名的
4.前后臺(tái)任務(wù)切換
fg + 作業(yè)編號(hào)切換指定任務(wù)到前臺(tái)
ctrl+z暫停任務(wù):bash自動(dòng)切換到前臺(tái)
bg +作業(yè)編號(hào)指定任務(wù)stop->run
當(dāng)我們進(jìn)行網(wǎng)絡(luò)通信的時(shí)候,如果服務(wù)器關(guān)機(jī)或注銷了,任務(wù)就可能會(huì)被清理,導(dǎo)致客戶端發(fā)送的消息無(wú)響應(yīng),這顯然與真正的服務(wù)器不一樣,我們需要把服務(wù)任務(wù)自成會(huì)話,自成進(jìn)程組,不受終端設(shè)備影響-----守護(hù)進(jìn)程
3.2 進(jìn)程,守護(hù)進(jìn)程化
#include <unistd.h>
pid_t setsid(void);//必須是非組長(zhǎng)調(diào)用
setsid 用于在一個(gè)新的會(huì)話中啟動(dòng)一個(gè)進(jìn)程。在運(yùn)行 setsid 命令時(shí),所啟動(dòng)的進(jìn)程將會(huì)脫離當(dāng)前的終端會(huì)話,并在一個(gè)新的會(huì)話中運(yùn)行,這樣它就不會(huì)受到終端會(huì)話關(guān)閉或掛起的影響,而可以持續(xù)運(yùn)行。
#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" //垃圾站,寫進(jìn)的內(nèi)容全丟棄
//自建一個(gè)會(huì)話,組長(zhǎng)就是自己,調(diào)用函數(shù)的不能是組長(zhǎng)
//調(diào)用完畢之后成為組長(zhǎng)
void daemonSelf(char* currPath=nullptr)
{
//1.讓調(diào)用進(jìn)程忽略掉異常的信號(hào)
//客戶端已經(jīng)退出,服務(wù)端再寫會(huì)崩潰
signal(SIGPIPE,SIG_IGN);
//2.不是組長(zhǎng),調(diào)用setsid
if(fork()>0) exit(0);
//子進(jìn)程---守護(hù)進(jìn)程又叫精靈進(jìn)程---本質(zhì)孤兒進(jìn)程
pid_t n=setsid();
assert(n!=-1);
//3.守護(hù)進(jìn)程是脫離終端的,關(guān)閉或重定向以前進(jìn)程默認(rèn)打開的文件
// /dev/null 垃圾站,寫進(jìn)的內(nèi)容全丟棄
int fd=open(DEV,O_RDWR);
if(fd>=0)
{
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
//012已經(jīng)執(zhí)行devil/null
close(fd);
}
else
{
close(0);
close(1);
close(2);
}
//4.可選:進(jìn)程執(zhí)行路徑發(fā)生更改
//chdir:將進(jìn)程的當(dāng)前工作目錄更改為 currpath 參數(shù)指定的目錄
if(!currPath) chdir(currPath);
}
在服務(wù)端初始化完畢之后,啟動(dòng)之前執(zhí)行daemonSelf()函數(shù),再啟動(dòng)服務(wù)端,查看進(jìn)程就可以得到以下信息
父進(jìn)程id為1證明這個(gè)進(jìn)程是一個(gè)孤兒進(jìn)程,而且可以發(fā)現(xiàn)這個(gè)進(jìn)程的PID,PGID,GID都一樣,這就是自成會(huì)話,自成進(jìn)程組的守護(hù)進(jìn)程?。?!
這個(gè)時(shí)候,即便關(guān)閉終端,只要不kill掉這個(gè)進(jìn)程,他就會(huì)在一直運(yùn)行響應(yīng)客戶端
四、TCP協(xié)議通信流程
建立連接的過(guò)程:
- 調(diào)用socket, 創(chuàng)建文件描述符;
- 調(diào)用connect, 向服務(wù)器發(fā)起連接請(qǐng)求;
- connect會(huì)發(fā)出SYN段并阻塞等待服務(wù)器應(yīng)答; (第一次)
- 服務(wù)器收到客戶端的SYN, 會(huì)應(yīng)答一個(gè)SYN-ACK段表示"同意建立連接"; (第二次)
- 客戶端收到SYN-ACK后會(huì)從connect()返回, 同時(shí)應(yīng)答一個(gè)ACK段; (第三次)
這個(gè)建立連接的過(guò)程, 通常稱為 三次握手
斷開連接的過(guò)程:
- 如果客戶端沒有更多的請(qǐng)求了, 就調(diào)用close()關(guān)閉連接, 客戶端會(huì)向服務(wù)器發(fā)送FIN段(第一次);
- 此時(shí)服務(wù)器收到FIN后, 會(huì)回應(yīng)一個(gè)ACK, 同時(shí)read會(huì)返回0 (第二次);
- read返回之后, 服務(wù)器就知道客戶端關(guān)閉了連接, 也調(diào)用close關(guān)閉連接, 這個(gè)時(shí)候服務(wù)器會(huì)向客戶端發(fā)送一個(gè)FIN; (第三次)
- 客戶端收到FIN, 再返回一個(gè)ACK給服務(wù)器; (第四次)
4.1 三次握手與四次揮手感性認(rèn)識(shí)
三次握手—建立連接:
女方對(duì)男方說(shuō)想談戀愛,男方答應(yīng)并問到什么時(shí)候開始,女方說(shuō)現(xiàn)在
四次揮手—斷開連接:
比如女方對(duì)男方說(shuō)離婚,男方回復(fù)離婚
同時(shí)男方反應(yīng)一會(huì)覺得不行,憑什么你先對(duì)我說(shuō),我也得休了你,然后對(duì)女方說(shuō)離婚,女方回復(fù)離婚文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-458086.html
TCP三握四揮傳送門>>>文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-458086.html
到了這里,關(guān)于linux【網(wǎng)絡(luò)編程】TCP協(xié)議通信模擬實(shí)現(xiàn)、日志函數(shù)模擬、守護(hù)進(jìn)程化、TCP協(xié)議通信流程、三次握手與四次揮手的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!