Linux之TCP和守護進程詳解
一、TCP網絡程序
1.1 TCP服務端
成員變量:
int _listensock; // 監(jiān)聽的文件描述符 string _ip; // 服務端ip uint16_t _port; // 端口號 bool _isrunning; // 服務器是否在運行
1.1.1 InitServer-創(chuàng)建服務端
1、創(chuàng)建套接字socket

- socket()打開一個網絡通訊端口,如果成功的話,就像open()一樣返回一個文件描述符;
- 應用程序可以像讀寫文件一樣用read/write在網絡上收發(fā)數據;
- 如果socket()調用出錯則返回-1;
- 對于IPv4, family參數指定為AF_INET;
- 對于TCP協(xié)議,type參數指定為SOCK_STREAM, 表示面向流的傳輸協(xié)議
- protocol參數的介紹從略,指定為0即可。
2、綁定套接字bind

- 服務器程序所監(jiān)聽的網絡地址和端口號通常是固定不變的,客戶端程序得知服務器程序的地址和端口號后 就可以向服務器發(fā)起連接; 服務器需要調用bind綁定一個固定的網絡地址和端口號;
- bind()成功返回0,失敗返回-1。
- bind()的作用是將參數sockfd和myaddr綁定在一起, 使sockfd這個用于網絡通訊的文件描述符監(jiān)聽myaddr所描述的地址和端口號;
- struct sockaddr *是一個通用指針類型,myaddr參數實際上可以接受多種協(xié)議的sockaddr結 構體,而它們的長度各不相同,所以需要第三個參數addrlen指定結構體的長度;
我們的程序中對myaddr參數是這樣初始化的:

注意:其實大部分的接口都已經幫我們考慮到大小端的問題了,只不過ip和port需要我們寫到OS里,所以需要我們自己去轉化!!
- (1)將整個結構體清零;
- (2)設置地址類型為AF_INET;
- (3)網絡地址為INADDR_ANY(或者是0.0.0.0), 這個宏表示本地的任意IP地址,因為服務器可能有多個網卡,每個網卡也可能綁 定多個IP地址, 這樣設置可以在所有的IP地址上監(jiān)聽,直到與某個客戶端建立了連接時才確定下來到底用哪個IP地址;
- (4)端口號為SERV_PORT, 我們定義為8080;
這兩步和之前UDP的基本一樣

3、TCP是面向連接的,服務器一般都是一個比較被動的狀態(tài),要等待客戶端和他建立連接關系。所以他需要不斷保持一個監(jiān)聽的狀態(tài) listen

listen()聲明sockfd處于監(jiān)聽狀態(tài), 并且最多允許有backlog個客戶端處于連接等待狀態(tài), 如果接收到更多 的連接請求就忽略, 這里設置不會太大(一般是5),
- listen()成功返回0,失敗返回-1;

全部代碼:
void InitServer() // 創(chuàng)建服務器
{
// 1/創(chuàng)建套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0); // 面向字節(jié)流;
if (_listensock < 0)
{
lg(Fatal, "create socket,errno:%d,errstring:%s", errno, strerror(errno));
exit(SocketError);
}
lg(Info, "create socket success,_listsock:%d", _listensock);
// 2/開始綁定
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 先清空,然后再填進去
local.sin_family = AF_INET;
local.sin_port = htons(_port); // 轉網絡序列
inet_aton(_ip.c_str(), &local.sin_addr);
// 開始綁定
if (bind(_listensock, (sockaddr *)&local, sizeof(local)) < 0) // 如果綁定失敗
{
lg(Fatal, "bind errno,errno:%d,errstring:%s", errno, strerror(errno));
exit(BindError);
}
lg(Info, "bind socket success,_listsock:%d", _listensock);
// 3/tcp和udp的區(qū)別就是要面向連接 要被動地等待別人來連接
if (listen(_listensock, backlog) < 0)
{
lg(Fatal, "listen errno,errno:%d,errstring:%s", errno, strerror(errno));
}
lg(Info, "listen socket success,_listsock:%d", _listensock);
}1.1.2 Run-運行服務器(單進程版)
1、接收客戶端的連接請求 accept

- 三次握手完成后, 服務器調用accept()接受連接;
- 如果服務器調用accept()時還沒有客戶端的連接請求,就阻塞等待直到有客戶端連接上來;
- addr是一個傳出參數,accept()返回時傳出客戶端的地址和端口號;
- 如果給addr 參數傳NULL,表示不關心客戶端的地址;
- addrlen參數是一個傳入傳出參數(value-result argument), 傳入的是調用者提供的, 緩沖區(qū)addr的長度 以避免緩沖區(qū)溢出問題, 傳出的是客戶端地址結構體的實際長度(有可能沒有占滿調用者提供的緩沖區(qū));
我們的服務器程序結構是這樣的:


accept的返回值是什么意義??
我們會發(fā)現accept返回的也是一個文件描述符,那么這個文件描述符跟我們剛開始的那個listensock是什么關系呢??
舉例子:假如我們在假期去一個旅游圣地,到中午的時候很多當地的餐館為了生意都會安排人站在外頭去拉客,比方說你們一行人走到一家魚莊面前,這時候魚莊門口有個張三馬上靠過來開始給你介紹這里餐館的特色,邀請你去魚莊吃飯,這個時候你們正好也餓了于是就進去了 ,但是你們進去的時候張三并沒有進去,而是喊了李四這個服務員過來給你們服務,自己又繼續(xù)出去拉客了,后來張三又不斷拉來新的客人,又不斷有王五,趙六……服務員也出來了,所以張三只關注于拉客,而李四等服務員只提供服務,他們各自專注著自己的事情卻可以把整個魚莊經營得非常好?。?!
所以我們之前的listensock就是我們的張三,他就是專門負責和客戶端建立連接的,而accept返回值的sockfd是就相當于是我們的李四,是專門給客戶端提供服務的!!
2、開始給客戶端提供服務


我們用telnet模擬客戶端就可以進行測試了?。?/span>


但是這樣有一個很尷尬的地方就是,我們當前這個單進程版的必須等到這個服務結束了才會去進行下一個客戶端的連接,這樣顯然是不符合我們的要求的!(有點像客流量很多 但是餐館只有一張桌子 這樣效率很低?。。?/span>!所以我們接下來要嘗試 多進程版、多線程版、線程池版
1.1.3 Run-運行服務器(多進程版)
多進程思路:讓子進程替我去完成工作,而我繼續(xù)去響應鏈接??!
(1)父進程把文件描述符表拷貝給子進程后,父進程就要把sockfd給關了(讓服務完全由子進程去做,如果子進程退出了意味著服務結束,這樣正好可以把這個文件給關了) 而子進程可以把listensockfd給關了(讓鏈接完全由父進程去做,防止子進程誤操作)
(2)但是如果父進程阻塞等待的話,又會和單進程一樣,而如果用非阻塞輪詢又會浪費cpu資源,且增加程序設計的復雜性 所以我們要思考其他辦法!!
方法1:讓孫子進程去做 然后子進程退出 這樣的話父進程立馬可以返回 而孫子進程會被系統(tǒng)領養(yǎng) 由系統(tǒng)回收

方法2:直接將SIGCHLD信號設成SIG_IGN 那么系統(tǒng)就不會把退出的進程轉成僵尸進程

1.1.4 Run-運行服務器 (多線程版)
但是多進程太耗費資源了??!所以我們應該考慮多線程版?。?/span>
(1)多線程不需要關閉文件描述符 因為是共享的所以沒有多余的
(2)如果join的話又會阻塞住,所以我們可以直接將線程給分離了,這樣主線程就不關心了!
(3)定義一個類將屬性傳給線程


如果線程調用的函數寫在里面,默認有this指針,所以必須把他設置成靜態(tài)成員函數??!
但是靜態(tài)成員函數不能調用非靜態(tài)成員函數,所以我們可以把對象指針傳進去,通過這個對象指針來調用成員函數。
1.1.5 Run-運行服務器(線程池版漢英翻譯)
我不希望你客戶端連接成功后我才去創(chuàng)建一個線程,而一個客戶斷開了又得釋放進程,這樣效率太低了!!而且我不想給你提供這種長服務(就是你當前客戶端如果請求太多的話,我就得一直專門服務你,這就是長服務),而多線程并不適合長服務,因為線程的個數是確定的,不能讓你一直給一個客戶端服務,所以我們要嘗試把他修改成短服務(就是這個客戶端一旦接受了你的一次請求他就斷掉 繼續(xù)去服務別的客戶端)??!
所以我們(1)一方面需要通過線程池來避免線程被重復創(chuàng)建和釋放的過程,(2)另一方面把長服務設置成短服務!!(3)然后將具體任務封裝起來交給線程池去完成,這樣可以解耦
我們可以將這個任務設置成英漢翻譯
dict.txt
apple:蘋果... banana:香蕉... red:紅色... yellow:黃色... the: 這 be: 是 to: 朝向/給/對 and: 和 I: 我 in: 在...里 that: 那個 have: 有 will: 將 for: 為了 but: 但是 as: 像...一樣 what: 什么 so: 因此 he: 他 her: 她 his: 他的 they: 他們 we: 我們 their: 他們的 his: 它的 with: 和...一起 she: 她 he: 他(賓格) it: 它
Init.hpp(讀取一個文件 然后分割到哈希表中)
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"
const std::string dictname = "./dict.txt";
const std::string sep = ":";
//yellow:黃色...
static bool Split(std::string &s, std::string *part1, std::string *part2)
{
auto pos = s.find(sep);
if(pos == std::string::npos) return false;
*part1 = s.substr(0, pos);
*part2 = s.substr(pos+1);
return true;
}
class Init
{
public:
Init()
{
std::ifstream in(dictname);
if(!in.is_open())
{
lg(Fatal, "ifstream open %s error", dictname.c_str());
exit(1);
}
std::string line;
while(std::getline(in, line))
{
std::string part1, part2;
Split(line, &part1, &part2);
dict.insert({part1, part2});
}
in.close();
}
std::string translation(const std::string &key)
{
auto iter = dict.find(key);
if(iter == dict.end()) return "Unknow";
else return iter->second;
}
private:
std::unordered_map<std::string, std::string> dict;
};Task.hpp 任務
#pragma once
#include <iostream>
#include <string>
#include "Log.hpp"
#include "Init.hpp"
extern Log lg;
Init init;
class Task
{
public:
Task(int sockfd, const std::string &clientip, const uint16_t &clientport)
: sockfd_(sockfd), clientip_(clientip), clientport_(clientport)
{
}
Task()
{
}
void run()
{
// 測試代碼
char buffer[4096];
// Tcp是面向字節(jié)流的,你怎么保證,你讀取上來的數據,是"一個" "完整" 的報文呢?
ssize_t n = read(sockfd_, buffer, sizeof(buffer)); // BUG?
if (n > 0)
{
buffer[n] = 0;
std::cout << "client key# " << buffer << std::endl;
std::string echo_string = init.translation(buffer);
// sleep(5);
// // close(sockfd_);
// lg(Warning, "close sockfd %d done", sockfd_);
// sleep(2);
n = write(sockfd_, echo_string.c_str(), echo_string.size()); // 100 fd 不存在
if(n < 0)
{
lg(Warning, "write error, errno : %d, errstring: %s", errno, strerror(errno));
}
}
else if (n == 0)
{
lg(Info, "%s:%d quit, server close sockfd: %d", clientip_.c_str(), clientport_, sockfd_);
}
else
{
lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd_, clientip_.c_str(), clientport_);
}
close(sockfd_);
}
void operator()()
{
run();
}
~Task()
{
}
private:
int sockfd_;
std::string clientip_;
uint16_t clientport_;
};線程池ThreadPool:
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>
struct ThreadInfo
{
pthread_t tid;
std::string name;
};
static const int defalutnum = 10;
template <class T>
class ThreadPool
{
public:
void Lock()
{
pthread_mutex_lock(&mutex_);
}
void Unlock()
{
pthread_mutex_unlock(&mutex_);
}
void Wakeup()
{
pthread_cond_signal(&cond_);
}
void ThreadSleep()
{
pthread_cond_wait(&cond_, &mutex_);
}
bool IsQueueEmpty()
{
return tasks_.empty();
}
std::string GetThreadName(pthread_t tid)
{
for (const auto &ti : threads_)
{
if (ti.tid == tid)
return ti.name;
}
return "None";
}
public:
static void *HandlerTask(void *args)
{
ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
std::string name = tp->GetThreadName(pthread_self());
while (true)
{
tp->Lock();
while (tp->IsQueueEmpty())
{
tp->ThreadSleep();
}
T t = tp->Pop();
tp->Unlock();
t();
}
}
void Start()
{
int num = threads_.size();
for (int i = 0; i < num; i++)
{
threads_[i].name = "thread-" + std::to_string(i + 1);
pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);
}
}
T Pop()
{
T t = tasks_.front();
tasks_.pop();
return t;
}
void Push(const T &t)
{
Lock();
tasks_.push(t);
Wakeup();
Unlock();
}
static ThreadPool<T> *GetInstance()
{
if (nullptr == tp_) // ???
{
pthread_mutex_lock(&lock_);
if (nullptr == tp_)
{
std::cout << "log: singleton create done first!" << std::endl;
tp_ = new ThreadPool<T>();
}
pthread_mutex_unlock(&lock_);
}
return tp_;
}
private:
ThreadPool(int num = defalutnum) : threads_(num)
{
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&cond_, nullptr);
}
~ThreadPool()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&cond_);
}
ThreadPool(const ThreadPool<T> &) = delete;
const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b=c
private:
std::vector<ThreadInfo> threads_;
std::queue<T> tasks_;
pthread_mutex_t mutex_;
pthread_cond_t cond_;
static ThreadPool<T> *tp_;
static pthread_mutex_t lock_;
};
template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;
template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;
1.1.6 服務端寫入時客戶端退出了怎么辦
對一個對端已經關閉的socket調用兩次write, 第二次將會生成SIGPIPE信號, 該信號默認結束進程.

1.1.7 服務端全部代碼
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h> //套接字類型的頭文件
#include <strings.h> //bzero的頭文件
#include <cstring>
#include <arpa/inet.h>
#include "Log.hpp"
#include <functional>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include"ThreadPool.hpp"
#include "Task.hpp"
using namespace std;
typedef function<string(const string &)> func_t;
Log lg; // 命令對象 用來打印日志信息
enum
{
UsageError = 1, // 使用有誤
SocketError, // 創(chuàng)建套接字有誤
BindError, // 綁定有誤
ListenError, // 監(jiān)聽有誤
};
const int defaultfd = -1;
const uint16_t defaultport = 8080;
const string defaultip = "0.0.0.0";
const int size = 1024;
const int backlog = 10; // 但是一般不要設置的太大
class ThreadData
{
public:
ThreadData(int fd, uint16_t &port, const string &ip, TcpServer *t) : sockfd(fd), clientport(port),clientip(ip), tsvr(t)
{
}
public:
int sockfd;
string clientip;
uint16_t clientport;
TcpServer *tsvr;//通過對象讓靜態(tài)成員函數調用 非靜態(tài)成員方法
};
class TcpServer
{
public:
TcpServer(uint16_t &port, const string &ip = defaultip) : _listensock(defaultfd), _port(port), _ip(ip), _isrunning(false)
{
}
static void *Routine(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
td->tsvr->Service(td->sockfd, td->clientport, td->clientip); //通過傳一個對象指針來調用類內的成員函數
delete td;
return nullptr;
}
void Service(int sockfd, uint16_t clientport, string clientip)
{
char buffer[size];
while (true)
{
// 服務端要先接收客戶端的數據
ssize_t n = read(sockfd, buffer, sizeof(buffer)); // 讀到緩沖區(qū)中
if (n > 0) // 大于0表示讀取成功 =0表示當前沒有數據可讀了 <0說明讀取失敗
{
buffer[n] = 0; // 把我們讀到的信息當成是字符串的形式來處理
cout << "client say#" << buffer << endl;
string echo_string = "tcpserver echo#";
echo_string += buffer;
// 對buffer簡單加工完之后往客戶端寫入
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if (n == 0) // 沒有什么數據可讀的了 所以八成是客戶端鏈接斷開了 我服務端不能崩
{
lg(Info, "client quit, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);
break;
}
else // 讀取失敗 可能是文件描述符被關閉了
{
lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);
}
}
}
void InitServer() // 創(chuàng)建服務器
{
// 1/創(chuàng)建套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0); // 面向字節(jié)流;
if (_listensock < 0)
{
lg(Fatal, "create socket,errno:%d,errstring:%s", errno, strerror(errno));
exit(SocketError);
}
lg(Info, "create socket success,_listsock:%d", _listensock);
// 2/開始綁定
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 先清空,然后再填進去
local.sin_family = AF_INET;
local.sin_port = htons(_port); // 轉網絡序列
inet_aton(_ip.c_str(), &local.sin_addr);
// 開始綁定
if (bind(_listensock, (sockaddr *)&local, sizeof(local)) < 0) // 如果綁定失敗
{
lg(Fatal, "bind errno,errno:%d,errstring:%s", errno, strerror(errno));
exit(BindError);
}
lg(Info, "bind socket success,_listsock:%d", _listensock);
// 3/tcp和udp的區(qū)別就是要面向連接 要被動地等待別人來連接
if (listen(_listensock, backlog) < 0)
{
lg(Fatal, "listen errno,errno:%d,errstring:%s", errno, strerror(errno));
}
lg(Info, "listen socket success,_listsock:%d", _listensock);
}
///
void Run() // 啟動服務器
{
ThreadPool<Task>::GetInstance()->Start();
signal(SIGPIPE,SIG_IGN);
_isrunning = true;
lg(Info, "tcpServer is running....");
// 1、accept嘗試獲取新鏈接
while (_isrunning) // 不斷獲取新鏈接
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(_listensock, (struct sockaddr *)&client, &len); // 這個id是用來做服務的?。?
if (sockfd < 0) // 如果獲取失敗 應該獲取下一個 而不是直接結束
{
lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));
continue;
}
lg(Info, "accept success,sockfd:%d", sockfd);
// 2、將客戶端的信息弄出來
uint16_t clientport = ntohs(client.sin_port);
char clientip[32]; // 輸出型參數
inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
// 根據新連接來進行通信服務-version單進程版
// Service(sockfd, clientport, clientip);
// close(sockfd);
// version2 多進程版-讓子進程幫我做服務,而我繼續(xù)去鏈接
// pid_t id=fork();
// if(id==0) //child 讓他去服務
// {
// close(_listensock);//關掉 防止誤操作
// if(fork()>0) exit(0);//子進程退掉 讓孫子進程來做
// Service(sockfd, clientport, clientip);//孫子進程此時已經被 system領養(yǎng)了
// close(sockfd);
// exit(0);
// }
// //father
// pid_t rid=waitpid(id,nullptr,0);//子進程一進去就退出了,所以父進程會馬上返回繼續(xù)去鏈接
// (void)rid;//rid沒用過 所以用一下防止警告
// //signal(SIGCHLD,SIG_IGN);
// version3 多線程版?。?
// ThreadData *td = new ThreadData(sockfd, clientport, clientip, this);
// pthread_t tid;
// pthread_create(&tid, nullptr, Routine, td);
//version4 線程池英漢詞典
Task t(sockfd, clientip, clientport);
ThreadPool<Task>::GetInstance()->Push(t);
}
}
~TcpServer()
{
}
private:
int _listensock; // 監(jiān)聽的文件描述符
string _ip; // 服務端ip
uint16_t _port; // 端口號
bool _isrunning; // 服務器是否在運行
};1.2 客戶端
客戶端幫我們發(fā)送connect請求的時候,會自動bind

客戶端需要調用connect()連接服務器;
connect和bind的參數形式一致, 區(qū)別在于bind的參數是自己的地址, 而connect的參數是對方的地址;
connect()成功返回0,出錯返回-1;
單進程版客戶端:
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
<< std::endl;
}
// ./tcpclient serverip serverport
int main(int argc,char* argv[]) //必須知道服務器的ip和端口號
{
if(argc!=3)
{
Usage(argv[0]);
exit(0);
}
string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
//1、第一步 創(chuàng)建套接字
int sockfd=socket(AF_INET, SOCK_STREAM, 0);
if(sockfd<0)
{
cerr << "socker error" << endl;
return 1;
}
//2/OS幫助們bind
struct sockaddr_in server;//輸出型參數
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
// server.sin_addr.s_addr = inet_addr(serverip.c_str());//字符串轉四字節(jié)
inet_pton(AF_INET,serverip.c_str(), &(server.sin_addr));
socklen_t len = sizeof(server);
//3、向服務端發(fā)送鏈接請求
int n=connect(sockfd,(struct sockaddr*)&server,len);
if(n<0)//如果鏈接失敗
{
cerr<<"connect error……" <<endl;
return 2;
}
//鏈接成功
cout<<"connect sucess"<<endl;
string message; //用來
char inbuffer[1024];//接收讀取的緩沖區(qū)
while(true)
{
cout<<"please enter@";
getline(cin,message); //將獲取的信息放到message中 發(fā)到服務端
// 1. 數據 2. 給誰發(fā)
ssize_t n=write(sockfd,message.c_str(),message.size());//可以直接通過write寫到文件里
//一般不會寫失敗,因為服務器一般都不關
//從文件里讀
ssize_t s = read(sockfd,inbuffer,sizeof(inbuffer));//讀到我們的緩沖區(qū)里
//會將結果帶回來
if(s > 0)
{
inbuffer[s] = 0;
cout << inbuffer << endl;
}
}
close(sockfd);
}線程池版英漢翻譯客戶端:
(1)需要改成短服務,所以鏈接在請求一次后就得斷掉,所以while循環(huán)必須寫在鏈接的前面
(2)我們平時掉線了 就是服務端和客戶端斷開了,這個時候我們客戶端要繼續(xù)嘗試跟服務端建立連接,當然也要限制連接次數 所以可以用一個do while循環(huán)放在鏈接那里
因為每處理一次請求就要斷掉,所以while循環(huán)必須寫到鏈接前面
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
<< std::endl;
}
// ./tcpclient serverip serverport
int main(int argc, char *argv[]) // 必須知道服務器的ip和端口號
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 2/OS幫助們bind
struct sockaddr_in server; // 輸出型參數
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
// server.sin_addr.s_addr = inet_addr(serverip.c_str());//字符串轉四字節(jié)
inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
// 3、向服務端發(fā)送鏈接請求
while (true)
{
int cnt = 5; // 重連次數
int isreconnect = false; // 是否要嘗試重連
// 創(chuàng)建套接字
int sockfd =0;
sockfd=socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
cerr << "socker error" << endl;
return 1;
}
do
{
int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0) // 如果鏈接失敗
{
isreconnect = true;
--cnt;
cerr << "connect error……" << endl;
}
else
break; // 鏈接成功就跳出去
} while (cnt && isreconnect);
if (cnt == 0)
{
cerr << "user ofline" << endl;
break;
}
// 鏈接成功
cout << "connect sucess" << endl;
string message; // 用來
char inbuffer[1024]; // 接收讀取的緩沖區(qū)
cout << "please enter@";
getline(cin, message); // 將獲取的信息放到message中 發(fā)到服務端
// 1. 數據 2. 給誰發(fā)
ssize_t n = write(sockfd, message.c_str(), message.size()); // 可以直接通過write寫到文件里
if (n < 0)
{
std::cerr << "write error..." << std::endl;
// break; 短服務不用出去
}
// 一般不會寫失敗,因為服務器一般都不關
// 從文件里讀
n = read(sockfd, inbuffer, sizeof(inbuffer)); // 讀到我們的緩沖區(qū)里
// 會將結果帶回來
if (n > 0)
{
inbuffer[n] = 0;
cout << inbuffer << endl;
}
close(sockfd);
}
return 0;
}當然如果執(zhí)行的是長服務的話,一旦寫入失敗就要break出去重連?。?/strong>
二、守護進程
服務端在我們ctrl c或者關掉xshell的時候就會被殺死,但是我們希望無論如何這個服務端是一直在跑的??!所以我們必須守護進程??!
2.1 Session和前后臺進程
每當一個用戶登錄的就是 默認就會形成一個session,然后分配一個bash進程
前臺進程后后臺進程的關鍵在于誰擁有鍵盤文件!!
1、執(zhí)行可執(zhí)行程序的時候在后面加個& 該進程就會變成后臺進程
2、通過jobs命令可以看到所有后臺任務

3、該序號叫做后臺進程任務號,我們可以使用fg+序號將后臺進程提到前臺
4、如果我們將一個后臺進程提到前臺之后后悔了,我們可以ctrl+z向前臺進程發(fā)送19號信號,此時當前臺進程被暫停時,bash進程就會自動移到前臺進程(因為在命令行中,前臺必須存在),而暫停的進程自動放到后臺。 然后通過bg+序號將因為暫停被放在后臺的進程恢復運行!
2.2 進程間關系



1、PGID叫進程組ID,一個組的是一樣的 ,只啟動一個進程的話就自成一組
2、sessionid用的就是bash進程的pid,而多個進程組在同一個session里面sid是一樣的
3、如果我們關掉OS,那么后臺進程會收到用戶登錄和退出的影響 因此我們需要守護進程化

2.3 如何做到
要嘗試自成一個會話??!


setsid 自成會話 不能是組長,所以我們必須fork出子進程,然后退出父進程,讓子進程執(zhí)行后面的代碼,所以 守護進程的本質也是孤兒進程!!
#pragma once
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
const std::string nullfile = "/dev/null";
void Daemon(const std::string &cwd = "")
{
// 1. 忽略其他異常信號
signal(SIGCLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
signal(SIGSTOP, SIG_IGN);
// 2. 將自己變成獨立的會話
if (fork() > 0)
exit(0);
setsid();
// 3. 更改當前調用進程的工作目錄
if (!cwd.empty())
chdir(cwd.c_str());
// 4. 標準輸入,標準輸出,標準錯誤重定向至/dev/null
int fd = open(nullfile.c_str(), O_RDWR);
if(fd > 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
}(1)忽略其他異常信號
(2)用setsid將自己變成獨立會話
(3)更改當前的工作目錄
(4)標準輸出、輸入、錯誤重定向到/dev/null (守護進程必須和他們解關聯,如果我們不往顯示器而是文件寫入的話還好,但如果我們直接關閉描述符的話顯然會導致printf和cout出錯!!而/dev/null就是相當于是一個垃圾桶文件,可以把不關心的內容丟到里面去)

所以我們將上述代碼在Run函數中運行 然后同時把我們的日志改成寫到文件中!!

如果我們想殺掉的話就得用kill -9 PID


2.4 為什么我們能遠程登錄Linux呢?
其實ssh就是守護進程,我們向他發(fā)送鏈接請求,認證后再登錄,然后分配一個會話,然后將命令再遠端執(zhí)行完后再返回給你

一般來說守護進程我們一般在他的名字后面加一個-D
三、TCP協(xié)議的通訊流程
3.1 TCP的三次握手和四次揮手

3.2 TCP通信全雙工
TCP是全雙工的 ,因為發(fā)送和接受緩沖區(qū)是分開的,多線程時雖然不能多人讀,但是支持同時讀寫?。?/p>


3.3 如何理解鏈接
對于服務器來說,同時存在大量連接,那么誰來打開、誰來關閉、連接狀態(tài)是什么,所以OS必須要先描述再組織來管理鏈接
總結
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
Linux系統(tǒng)配置靜態(tài)IP地址的詳細步驟
在安裝Linux后,系統(tǒng)的網絡IP地址默認是自動分配的,這將導致每次啟動Linux系統(tǒng)后,系統(tǒng)的IP地址都會發(fā)生改變,此文以CentOS7系統(tǒng)環(huán)境為例,詳細介紹如何配置Linux系統(tǒng)的靜態(tài)IP地址,需要的朋友可以參考下2024-04-04
centos7.2搭建LAMP環(huán)境的具體操作方法
下面小編就為大家?guī)硪黄猚entos7.2搭建LAMP環(huán)境的具體操作方法。小編覺得挺不錯的,現在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-08-08
linux如何編譯安裝新內核支持NTFS文件系統(tǒng)(以redhat7.2x64為例)
這篇文章主要介紹了linux如何編譯安裝新內核支持NTFS文件系統(tǒng)(以redhat7.2x64為例),具有一定的參考價值,感興趣的小伙伴們可以參考一下。2016-10-10
linux下SVN配置實現項目目錄自動更新以及源碼安裝的操作方法
下面小編就為大家分享一篇linux下SVN配置實現項目目錄自動更新以及源碼安裝的操作方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2017-12-12

