一、引入
UDP和TCP的區(qū)別:
對于TCP協(xié)議有幾個特點:
1?? 傳輸層協(xié)議
2?? 有連接(正式通信前要先建立連接)
3?? 可靠傳輸(在內(nèi)部幫我們做可靠傳輸工作)
4?? 面向字節(jié)流
對于UDP協(xié)議有幾個特點:
1?? 傳輸層協(xié)議
2?? 無連接
3?? 不可靠傳輸
4?? 面向數(shù)據(jù)報
可以看到TCP對比UDP會建立鏈接。
其他的接口跟UDP其實沒什么區(qū)別:【網(wǎng)絡(luò)編程】demo版UDP網(wǎng)絡(luò)服務(wù)器實現(xiàn)
二、服務(wù)端實現(xiàn)
2.1 創(chuàng)建套接字socket
在通信之前要先把網(wǎng)卡文件打開。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
RETURN VALUE
On success, a file descriptor for the new socket is returned.
On error, -1 is returned, and errno is set appropriately.
這個函數(shù)的作用是打開一個文件,把文件和網(wǎng)卡關(guān)聯(lián)起來。
參數(shù)介紹:
domain
:一個域,標(biāo)識了這個套接字的通信類型(網(wǎng)絡(luò)或者本地)。
只用關(guān)注上面兩個類,第一個AF_UNIX
表示本地通信,而AF_INET
表示網(wǎng)絡(luò)通信。type
:套接字提供服務(wù)的類型。
這一章我們講的式TCP,所以使用SOCK_STREAM
。protocol
:想使用的協(xié)議,默認(rèn)為0即可,因為前面的兩個參數(shù)決定了,就已經(jīng)決定了是TCP還是UDP協(xié)議了。
返回值:
成功則返回打開的文件描述符(指向網(wǎng)卡文件),失敗返回-1。
而從這里我們就聯(lián)想到系統(tǒng)中的文件操作,未來各種操作都要通過這個文件描述符,所以在服務(wù)端類中還需要一個成員變量表示文件描述符。
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include "log.hpp"
class TCPServer
{
static const uint16_t gport = 8080;
public:
TCPServer(cosnt uint16_t& port = gport)
: _sock(-1)
, _port(port)
{}
void InitServer()
{
_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_sockfd == -1)
{
std::cerr << "create socket error" << std::endl;
exit(1);
}
std::cout << "create socket success" << std::endl;
}
void start()
{}
private:
int _sock;
uint16_t _port;
};
2.2 綁定bind
#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
RETURN VALUE
Upon successful completion, bind() shall return 0;
otherwise, -1 shall be returned and errno set to indicate the error.
參數(shù)介紹:
socket
:創(chuàng)建套接字的返回值。address
:通用結(jié)構(gòu)體(【網(wǎng)絡(luò)編程】socket套接字有詳細(xì)介紹)。address_len
:傳入結(jié)構(gòu)體的長度。
所以我們要先定義一個sockaddr_in
結(jié)構(gòu)體填充數(shù)據(jù),在傳遞進(jìn)去。
然后就是跟UDP一樣,先初始化結(jié)構(gòu)體,再處理IP和端口。
要注意IP要綁定任意IP也就是INADDR_ANY
。
至于為什么再上一章【網(wǎng)絡(luò)編程】demo版UDP網(wǎng)絡(luò)服務(wù)器實現(xiàn)有過詳細(xì)講解。
void InitServer()
{
_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_sockfd == -1)
{
std::cerr << "create socket error" << std::endl;
exit(1);
}
std::cout << "create socket success" << std::endl;
struct sockaddr_in si;
// 初始化結(jié)構(gòu)體
bzero(&si, sizeof si);
si.sin_family = AF_INET;
si.sin_port = htons(_port);// 主機轉(zhuǎn)網(wǎng)絡(luò)序列
si.sin_addr.s_addr = INADDR_ANY;
if(bind(_sock, (struct sockaddr*)&si, sizeof si) < 0)
{
std::cout << "bind socket error" << std::endl;
exit(1);
}
std::cout << "bind socket success" << std::endl;
}
2.3 設(shè)置監(jiān)聽狀態(tài)listen
TCP跟UDP的不同在這里就體現(xiàn)了出來。
要把socket套接字的狀態(tài)設(shè)置為listen狀態(tài)。只有這樣才能一直獲取新鏈接,接收新的鏈接請求。
舉個例子:
我們買東西如果出現(xiàn)了問題會去找客服,如果客服不在那么就回復(fù)不了,所以規(guī)定了客服在工作的時候必須要時刻接收回復(fù)消息,這個客服所處的狀態(tài)就叫做監(jiān)聽狀態(tài)。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
RETURN VALUE
On success, zero is returned.
On error, -1 is returned, and errno is set appropriately.
關(guān)于第二個參數(shù)backlog后邊講TCP協(xié)議的時候介紹,目前先直接用。
static const int gbacklog = 10;
void InitServer()
{
_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_sock == -1)
{
std::cerr << "create socket error" << std::endl;
exit(1);
}
std::cout << "create socket success" << std::endl;
struct sockaddr_in si;
// 初始化結(jié)構(gòu)體
bzero(&si, sizeof si);
si.sin_family = AF_INET;
si.sin_port = htons(_port);// 主機轉(zhuǎn)網(wǎng)絡(luò)序列
si.sin_addr.s_addr = INADDR_ANY;
if(bind(_sock, (struct sockaddr*)&si, sizeof si) < 0)
{
std::cout << "bind socket error" << std::endl;
exit(1);
}
std::cout << "bind socket success" << std::endl;
// 設(shè)置監(jiān)聽狀態(tài)
if(listen(_sock, gbacklog) < 0)
{
std::cout << "listen socket error" << std::endl;
exit(1);
}
std::cout << "listen socket success" << std::endl;
}
2.4 獲取新鏈接accept
上面初始化完畢,現(xiàn)在開始就是要運行服務(wù)端,而TCP不能直接發(fā)數(shù)據(jù),因為它是面向鏈接的,必須要先建立鏈接。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
RETURN VALUE
On success, these system calls return a nonnegative integer that is a descriptor for the accepted socket.
On error, -1 is returned, and errno is set appropriately.
參數(shù)介紹:
sockfd
文件描述符,找到套接字addr
輸入輸出型參數(shù),是一個結(jié)構(gòu)體,用來獲取客戶端的信息。addrlen
輸入輸出型參數(shù),客戶端傳過來的結(jié)構(gòu)體大小。
返回值:
成功返回一個文件描述符
失敗返回-1
而我們知道sockfd本來就是一個文件描述符,那么這個返回的文件描述符是什么呢?
舉個例子:
我們?nèi)コ燥埖臅r候會發(fā)現(xiàn)每個店鋪門口都會有人來招攬顧客,這個人把我們領(lǐng)進(jìn)去門店后,然后他就會繼續(xù)站在門口繼續(xù)招攬顧客,而我們會有里面的服務(wù)員來招待我們,給我們提供服務(wù)。
這里的攬客的人就是sockfd,而服務(wù)員就是返回值的文件描述符。
意思就是sockfd的作用就是把鏈接從底層獲取上來,返回值的作用就是跟客戶端通信。
從這里就知道了成員變量中的_sock
并不是通信用的套接字,而是獲取鏈接的套接字。為了方便觀察,我們可以把所有的_sock換
成_listensock
。
void start()
{
while(1)
{
// 獲取新鏈接
struct sockaddr_in si;
socklen_t len = sizeof si;
int sock = accept(_listensock, (struct sockaddr*)&si, &len);
if(sock < 0)
{
// 獲取鏈接失敗無影響,繼續(xù)獲取即可
std::cout << "accept error, continue" << std::endl;
continue;
}
std::cout << "accept a new link success" << std::endl;
std::cout << "sock: " << sock << std::endl;
}
}
2.5 獲取信息與返回信息(文件操作)
上面獲取到了通信用的套接字sock,而因為TCP通信是面向字節(jié)流的,所以后續(xù)通信全部是用文件操作(IO),因為文件也是面向字節(jié)流的。
IO的操作我們可以封裝一個函數(shù)。
void ServerIO(int sock)
{
char buf[1024];
// 接收消息
while(1)
{
ssize_t n = read(sock, buf, sizeof buf - 1);
if(n > 0)
{
buf[n] = '\0';
std::cout << "read a message: " << buf << std::endl;
// 把消息發(fā)送回去
std::string outbuf;
outbuf += "Server[echo]#";
outbuf += buf;
write(sock, outbuf.c_str(), outbuf.size());
}
else if(n == 0)
{
// 代表客戶端退出
std::cout << "Client quit" << std::endl;
break;
}
}
}
當(dāng)IO完后要記得關(guān)閉文件描述符sock,不然會導(dǎo)致可用描述符越來越少。
驗證發(fā)現(xiàn)可以運行
三、客戶端實現(xiàn)
3.1 創(chuàng)建套接字socket
Socket可以看成在兩個程序進(jìn)行通訊連接中的一個端點,一個程序?qū)⒁欢涡畔懭隨ocket中,該Socket將這段信息發(fā)送給另外一個Socket中,使這段信息能傳送到其他程序中。
所以客戶端也需要一個套接字。
void initClient()
{
_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_sock < 0)
{
std::cout << "create socket error" << std::endl;
exit(1);
}
std::cout << "create socket success" << std::endl;
}
3.2 綁定問題
跟上一章一樣要綁定,但是不能顯示綁定。
3.3 發(fā)起鏈接connect
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
RETURN VALUE
If the connection or binding succeeds, zero is returned.
On error, -1 is returned, and errno is set appropriately.
參數(shù)說明:
這里的
addr
和addrlen
填入的是服務(wù)端信息。
在UDP通信中,客戶端在sendto的時候會自動綁定IP和port,TCP這里就是在connect的時候綁定。
void start()
{
struct sockaddr_in si;
bzero(&si, sizeof si);
si.sin_family = AF_INET;
si.sin_port = htons(_serverport);
si.sin_addr.s_addr = inet_addr(_serverip.c_str());
if(connect(_sock, (struct sockaddr*)&si, sizeof si) < 0)
{
std::cout << "connect socket error" << std::endl;
}
else
{
std::string msg;
while(1)
{
std::cout << "Please Enter#";
std::getline(std::cin, msg);
write(_sock, msg.c_str(), msg.size());
// 從服務(wù)端讀取數(shù)據(jù)
char buf[1024];
int n = read(_sock, buf, sizeof buf - 1);
if(n > 0)
{
buf[n] = '\0';
std::cout << buf << std::endl;
}
else
{
break;
}
}
}
}
最后在析構(gòu)的時候要關(guān)掉文件描述符。
~TCPClient()
{
if(_sock >= 0)
{
close(_sock);
}
}
服務(wù)端:
客戶端:
但此時如果我們在用一個客戶端鏈接,會發(fā)現(xiàn)無法通信,除非第一個客戶端退出。
這是因為獲取新鏈接后會進(jìn)入ServerIo中死循環(huán)。只要這個客戶端不退出,就會一直給這個客戶端提供服務(wù)。
那怎么保證多個客戶端并行呢?
3.4 客戶端并行
3.4.1 多進(jìn)程版
因為fork后子進(jìn)程會復(fù)制父進(jìn)程的文件描述符。
這里注意子進(jìn)程并不需要_listensock
文件描述符,所以最好關(guān)閉。
pid_t id = fork();
if(id == 0)// child
{
close(_listensock);
ServerIO(sock);
close(sock);
exit(1);
}
接下來父進(jìn)程怎么辦呢?是等待嗎?
如果父進(jìn)程等待的話又會導(dǎo)致上面的情況,子進(jìn)程不退出父進(jìn)程就一直等待。
子進(jìn)程退出時,會給父進(jìn)程發(fā)送一個SIGCHLD,17號信號。所以有一種解決辦法就是用signal
函數(shù),在回調(diào)函數(shù)中把waitpid的參數(shù)設(shè)置為-1(等待任意進(jìn)程),就可以回收。
現(xiàn)在我們不用這種辦法,我們可以這么寫:
pid_t id = fork();
if(id == 0)// child
{
close(_listensock);
if(fork() > 0) exit(1);
ServerIO(sock);
close(sock);
exit(1);
}
// father
pid_t ret = waitpid(id, nullptr, 0);
if(ret > 0)
{
std::cout << "wait success" << ret << std::endl;
}
這里的意思就是創(chuàng)建孫子進(jìn)程,父進(jìn)程直接退出,讓孫子進(jìn)程執(zhí)行ServerIO,此時孫子進(jìn)程就會被操作系統(tǒng)收養(yǎng),不用我們管,而父進(jìn)程退出,外邊的父進(jìn)程也等待成功了。
結(jié)果演示:
客戶端:
服務(wù)端:
其實下面的等待可以不用等待,因為SIGCHLD信號默認(rèn)的處理方式是忽略。
這里看到客戶端退出了但是文件描述符并沒有被回收。
這里的原因是我們只關(guān)閉了子進(jìn)程的文件描述符,沒有關(guān)閉父進(jìn)程:
3.4.2 多線程版
struct ThreadData
{
TCPServer* _self;
int _sock;
};
void start()
{
while(1)
{
// 獲取新鏈接
struct sockaddr_in si;
socklen_t len = sizeof si;
int sock = accept(_listensock, (struct sockaddr*)&si, &len);
if(sock < 0)
{
// 獲取鏈接失敗無影響,繼續(xù)獲取即可
std::cout << "accept error, continue" << std::endl;
continue;
}
std::cout << "accept a new link success" << std::endl;
std::cout << "sock: " << sock << std::endl;
// 多線程
pthread_t tid;
ThreadData* td = new ThreadData({this, sock});
pthread_create(&tid, nullptr, thread_start, td);
}
}
static void* thread_start(void* args)
{
// 線程分離
pthread_detach(pthread_self());
ThreadData* tp = static_cast<ThreadData*>(args);
tp->_self->ServerIO(tp->_sock);
close(tp->_sock);
delete tp;
}
3.4.3 線程池版
前面我們寫過線程池【linux】基于單例模式實現(xiàn)線程池,這里直接拿來用即可。
這里就要修改一下代碼,因為ServerIO可以不屬于類,所以可以把ServerIO放在任務(wù)Task.hpp
中。文章來源:http://www.zghlxwxcb.cn/news/detail-462008.html
// Task.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstdio>
void ServerIO(int sock)
{
char buf[1024];
// 接收消息
while(1)
{
ssize_t n = read(sock, buf, sizeof buf - 1);
if(n > 0)
{
buf[n] = '\0';
std::cout << "read a message: " << buf << std::endl;
// 把消息發(fā)送回去
std::string outbuf;
outbuf += "Server[echo]#";
outbuf += buf;
write(sock, outbuf.c_str(), outbuf.size());
}
else if(n == 0)
{
// 代表客戶端退出
std::cout << "Client quit" << std::endl;
break;
}
}
}
class Task
{
typedef std::function<void(int)> func_t;
public:
Task()
{}
Task(int sock, func_t func)
: _sock(sock)
, _func(func)
{}
void operator()()
{
_func(_sock);
}
std::string tostringTask()
{
char buf[64];
snprintf(buf, sizeof buf, "%d %c %d = ?", _x, _op, _y);
return buf;
}
private:
int _sock;
func_t _func;
};
// ThreadPool.hpp
#pragma once
#include <vector>
#include <queue>
#include <mutex>
#include "mythread.hpp"
#include "mymutex.hpp"
#include "Task.hpp"
using std::cout;
using std::endl;
const int N = 5;
template <class T>
class ThreadPool;
template <class T>
struct ThreadData
{
ThreadPool<T>* _tp;
std::string _name;
ThreadData(ThreadPool<T>* tp, const std::string& name)
: _tp(tp)
, _name(name)
{}
};
template <class T>
class ThreadPool
{
private:
static void* handlerTask(void* args)
{
ThreadData<T>* tdp = static_cast<ThreadData<T>*>(args);
while(true)
{
tdp->_tp->lockqueue();
while(tdp->_tp->isqueueempty())
{
tdp->_tp->threadwait();
}
T t = tdp->_tp->pop();
tdp->_tp->unlockqueue();
t();
}
delete tdp;
}
void lockqueue() volatile
{
pthread_mutex_lock(&_mutex);
}
void unlockqueue() volatile
{
pthread_mutex_unlock(&_mutex);
}
bool isqueueempty() volatile
{
return _tasks.empty();
}
void threadwait() volatile
{
pthread_cond_wait(&_cond, &_mutex);
}
T pop() volatile
{
T res = _tasks.front();
_tasks.pop();
return res;
}
ThreadPool(int num = 5)
: _num(num)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
// 創(chuàng)建線程
for(int i = 0; i < _num; i++)
{
_threads.push_back(new Thread());
}
}
ThreadPool(const ThreadPool<T>& ) = delete;
ThreadPool<T> operator=(const ThreadPool<T>&) = delete;
public:
void start() volatile
{
for(auto& t : _threads)
{
ThreadData<T>* td = new ThreadData<T>(this, t->GetName());
t->start(handlerTask, td);
}
}
void push(const T& in) volatile
{
LockAuto lock(&_mutex);
_tasks.push(in);
// 喚醒池中的一個線程
pthread_cond_signal(&_cond);
}
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
for(auto & e : _threads)
{
delete e;
}
}
volatile static ThreadPool<T>* GetSingle()
{
if(_tp == nullptr)
{
_singlelock.lock();
if(_tp == nullptr)
{
_tp = new ThreadPool<T>();
}
_singlelock.unlock();
}
return _tp;
}
private:
int _num;// 線程數(shù)量
std::vector<Thread*> _threads;
std::queue<T> _tasks;// 任務(wù)隊列
pthread_mutex_t _mutex;// 保護任務(wù)隊列
pthread_cond_t _cond;
volatile static ThreadPool<T>* _tp;
static std::mutex _singlelock;
};
template <class T>
volatile ThreadPool<T>* ThreadPool<T>::_tp = nullptr;
template <class T>
std::mutex ThreadPool<T>::_singlelock;
// TCPServer.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>
#include <strings.h>
#include <cstdlib>
#include <sys/wait.h>
#include <pthread.h>
#include "ThreadPool.hpp"
#include "log.hpp"
class TCPServer;
struct ThreadData
{
TCPServer* _self;
int _sock;
};
class TCPServer
{
static const uint16_t gport = 8080;
static const int gbacklog = 10;
public:
TCPServer(const uint16_t& port = gport)
: _listensock(-1)
, _port(port)
{}
void InitServer()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if(_listensock == -1)
{
std::cerr << "create socket error" << std::endl;
exit(1);
}
std::cout << "create socket success" << std::endl;
struct sockaddr_in si;
// 初始化結(jié)構(gòu)體
bzero(&si, sizeof si);
si.sin_family = AF_INET;
si.sin_port = htons(_port);// 主機轉(zhuǎn)網(wǎng)絡(luò)序列
si.sin_addr.s_addr = INADDR_ANY;
if(bind(_listensock, (struct sockaddr*)&si, sizeof si) < 0)
{
std::cout << "bind socket error" << std::endl;
exit(1);
}
std::cout << "bind socket success" << std::endl;
// 設(shè)置監(jiān)聽狀態(tài)
if(listen(_listensock, gbacklog) < 0)
{
std::cout << "listen socket error" << std::endl;
exit(1);
}
std::cout << "listen socket success" << std::endl;
}
void start()
{
// 線程池初始化
ThreadPool<Task>::GetSingle()->start();
while(1)
{
// 獲取新鏈接
struct sockaddr_in si;
socklen_t len = sizeof si;
int sock = accept(_listensock, (struct sockaddr*)&si, &len);
if(sock < 0)
{
// 獲取鏈接失敗無影響,繼續(xù)獲取即可
std::cout << "accept error, continue" << std::endl;
continue;
}
std::cout << "accept a new link success" << std::endl;
std::cout << "sock: " << sock << std::endl;
// 線程池
ThreadPool<Task>::GetSingle()->push(Task(sock, ServerIO));
// 多線程
// pthread_t tid;
// ThreadData* td = new ThreadData({this, sock});
// pthread_create(&tid, nullptr, thread_start, td);
// 多進(jìn)程
// pid_t id = fork();
// if(id == 0)// child
// {
// close(_listensock);
// if(fork() > 0) exit(1);
// ServerIO(sock);
// close(sock);
// exit(1);
// }
// close(sock);
// father
// pid_t ret = waitpid(id, nullptr, 0);
// if(ret > 0)
// {
// std::cout << "wait success " << ret << std::endl;
// }
// ServerIO(sock);
// // 關(guān)閉使用完的文件描述符
// close(sock);
}
}
static void* thread_start(void* args)
{
// 線程分離
pthread_detach(pthread_self());
ThreadData* tp = static_cast<ThreadData*>(args);
tp->_self->ServerIO(tp->_sock);
close(tp->_sock);
delete tp;
}
private:
int _listensock;
uint16_t _port;
};
四、總結(jié)
對比UDP服務(wù)器,TCP服務(wù)器多了獲取新鏈接和監(jiān)聽的操作,而因為TCP是面向字節(jié)流的,所以接收和發(fā)送數(shù)據(jù)都是IO操作,也就是文件操作。文章來源地址http://www.zghlxwxcb.cn/news/detail-462008.html
到了這里,關(guān)于【網(wǎng)絡(luò)編程】demo版TCP網(wǎng)絡(luò)服務(wù)器實現(xiàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!