目錄
文件1:tcpServer.cc
文件2:tcpServer.hpp
1.提出日志概念 -- 在后續(xù)完善
日志格式 --?暫定簡單的打印功能
2.創(chuàng)建套接字
SOCK_STREAM -- socket參數(shù)
3.bind自己的套接字
4.設(shè)置socket 為監(jiān)聽狀態(tài) *
新接口1:listen
函數(shù)1:initServer()
新接口2:accept *
接口1:read
接口2:write
文件描述符本質(zhì)是數(shù)組下標 -- 有限
ulimit -- 查看本機可以開多少個文件描述符
函數(shù)2:serviceIO()
至此基本的功能完成 -- 測試1
準備工作
文件3:tcpClient.cc
文件4:tcpClient.hpp
函數(shù)3:initClient()
新接口3:connect *
函數(shù)4:start()
至此TCP通信的功能完成 -- 測試2
全部代碼
log.hpp
makefile
tcpClient.cc
tcpClient.hpp
tcpServer.cc
tcpServer.hpp
直接代碼開整
文件1:tcpServer.cc
準備階段 -- 目前和UDP是一樣的
#include "tcpServer.hpp"
#include <memory>
using namespace std;
using namespace server;
static void Usage(string proc)
{
cout << "Usage:\n\t" << proc << " local_port\n\n"; // 命令提示符
}
// tcp服務器,啟動和 udp server 一模一樣
// ./tcpserver lock_port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<TcpServer> tsvr(new TcpServer());
tsvr->initServer();
tsvr->start();
return 0;
}
文件2:tcpServer.hpp
1.提出日志概念 -- 在后續(xù)完善
日志格式 --?暫定簡單的打印功能
這個日志在后序完善TCP之后再進行修改,現(xiàn)在只實現(xiàn)簡單的打印功能
2.創(chuàng)建套接字
SOCK_STREAM -- socket參數(shù)
3.bind自己的套接字
????????這里有個細節(jié),我們會發(fā)現(xiàn)當我們接受數(shù)據(jù)的時候是不需要主機轉(zhuǎn)網(wǎng)路序列的,因為關(guān)于IO類的接口,內(nèi)部都幫我們實現(xiàn)了這一功能,這里不幫我們做是因為我們傳入的是一個結(jié)構(gòu)體,系統(tǒng)做不到
4.設(shè)置socket 為監(jiān)聽狀態(tài) *
新接口1:listen
底層鏈接的長度+1,先不管他,在后序講原理再講述
函數(shù)1:initServer()
void initServer()
{
// 1. 創(chuàng)建socket文件套接字對象 -- 流式套接字
_sock = socket(AF_INET, SOCK_STREAM, 0); // 第三個參數(shù)默認 0
if (_sock < 0)
{
logMessage(FATAL, "create socket error");
exit(SOCKET_ERR);
}
logMessage(NORMAL, "create socket success");
// 2.bind綁定自己的網(wǎng)路信息 -- 注意包含頭文件
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port); // 這里有個細節(jié),我們會發(fā)現(xiàn)當我們接受數(shù)據(jù)的時候是不需要主機轉(zhuǎn)網(wǎng)路序列的,因為關(guān)于IO類的接口,內(nèi)部都幫我們實現(xiàn)了這一功能,這里不幫我們做是因為我們傳入的是一個結(jié)構(gòu)體,系統(tǒng)做不到
local.sin_addr.s_addr = INADDR_ANY; // 接受任意ip地址
if (bind(_sock, (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) -- TCP與UDP不同,它先要建立鏈接之后,TCP是面向鏈接的,后面還會有“握手”過程
if (listen(_sock, gbacklog) < 0) // 第二個參數(shù)backlog后面再填這個坑
{
logMessage(FATAL, "listen socket error");
exit(LISTEN_ERR);
}
logMessage(FATAL, "listen socket success");
}
注意這里是起始版本,在認識下面的一個接口的時候,需要整改
新接口2:accept *
? ? ? ? 一個比喻:就像一家飯店的門口招呼人的張三,當張三從外邊招呼人進來的時候,就向飯店里面喊人,讓李四去服務客人,但是張三不會進來,又返回去在門口拉客
? ? ? ? 因為隨著客戶端的不斷增多,TCP服務器上可能存在多個套接字,就像飯店里面會有多個客人有多個服務員
至此我們需要把之前的_sock 修改為 _listensock
至此我們獲取的sock就是一個文件操作符,可以使用文件操作類的函數(shù)進行處理,例如read之類的
接口1:read
從一個文件描述符中讀取
接口2:write
文件描述符本質(zhì)是數(shù)組下標 -- 有限
ulimit -- 查看本機可以開多少個文件描述符
函數(shù)2:serviceIO()
void serviceIO(int sock)
{
// 先用最簡單的,讀取再寫回去
char buffer[1024];
while (true)
{
ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
if (n > 0)
{
// 截至目前,我們把讀到的數(shù)據(jù)當作字符串
buffer[n] = 0;
std::cout << "recv message: " << buffer << std::endl;
std::string outbuffer = buffer;
outbuffer += "server[echo]";
write(sock, outbuffer.c_str(), outbuffer.size()); // 在多路轉(zhuǎn)接的時候再詳談write的返回值
}
else if(n == 0)
{
// 代表client退出 -- 把它想象成一個建立好的管道,客戶端不寫了,并且把它的文件描述符關(guān)了,讀端就會像管道一樣讀到 0 TCP同理
logMessage(NORMAL, "client quit, me too!");
}
}
}
至此基本的功能完成 -- 測試1
準備工作
文件3:tcpClient.cc
調(diào)用邏輯?
#include "tcpClient.hpp"
#include <memory>
using namespace std;
static void Usage(string proc)
{
cout << "Usage:\n\t" << proc << " serverip serverport\n\n"; // 命令提示符
}
// ./tcpclient serverip serverport 調(diào)用邏輯
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> tcli(new TcpClient(serverip, serverport));
tcli->initClient();
tcli->start();
return 0;
}
文件4:tcpClient.hpp
函數(shù)3:initClient()
void initClient()
{
// 1. 創(chuàng)建socket
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0)
{
// 客戶端也可以有日志,不過這里就不再實現(xiàn)了,直接打印錯誤
std::cout << "socket create error" << std::endl;
exit(2);
}
// 2. tcp的客戶端要不要bind? 要的! 但是不需要顯示bind,這里的client port要讓OS自定!
// 3. 要不要listen? -- 不需要!客戶端不需要建立鏈接
// 4. 要不要accept? -- 不要!
// 5. 要什么? 要發(fā)起鏈接!
}
新接口3:connect *
函數(shù)4:start()
void start()
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(_serverport);
server.sin_addr.s_addr = inet_addr(_serverip.c_str());
if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0)
{
std::cerr << "socket connect error" << std::endl;
}
else
{
std::string msg;
while (true)
{
std::cout << "Enter# ";
std::getline(std::cin, msg);
write(_sock, msg.c_str(), msg.size());
char buffer[NUM];
int n = read(_sock, buffer, sizeof(buffer) - 1);
if (n > 0)
{
// 目前我們把讀到的數(shù)據(jù)當成字符串, 截至目前
buffer[n] = 0;
std::cout << "Server回顯# " << buffer << std::endl;
}
else
{
break;
}
}
}
}
至此TCP通信的功能完成 -- 測試2
????????但是至此,我們所寫的不過是一個單進程版的,所以會出現(xiàn)下面的情況,后續(xù)需要進一步修改成為多進程形式的
文章來源:http://www.zghlxwxcb.cn/news/detail-501312.html
全部代碼
log.hpp
#pragma once
#include <iostream>
#include <string>
// 定義五種不同的信息
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3 //一種不影響服務器的錯誤
#define FATAL 4 //致命錯誤
void logMessage(int level, const std::string message)
{
// 格式如下
// [日志等級] [時間戳/時間] [pid] [message]
// [FATAL0] [2023-06-11 16:46:07] [123] [創(chuàng)建套接字失敗]
// 暫定
std::cout << message << std::endl;
}
makefile
cc=g++
.PHONY:all
all:tcpserver tcpclient
tcpclient:tcpClient.cc
$(cc) -o $@ $^ -std=c++11
tcpserver:tcpServer.cc
$(cc) -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f tcpserver tcpclient
tcpClient.cc
#include "tcpClient.hpp"
#include <memory>
using namespace std;
static void Usage(string proc)
{
cout << "Usage:\n\t" << proc << " serverip serverport\n\n"; // 命令提示符
}
// ./tcpclient serverip serverport 調(diào)用邏輯
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> tcli(new TcpClient(serverip, serverport));
tcli->initClient();
tcli->start();
return 0;
}
tcpClient.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define NUM 1024
class TcpClient
{
public:
TcpClient(const std::string &serverip, const uint16_t &port)
: _sock(1), _serverip(serverip), _serverport(port)
{
}
void initClient()
{
// 1. 創(chuàng)建socket
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0)
{
// 客戶端也可以有日志,不過這里就不再實現(xiàn)了,直接打印錯誤
std::cout << "socket create error" << std::endl;
exit(2);
}
// 2. tcp的客戶端要不要bind? 要的! 但是不需要顯示bind,這里的client port要讓OS自定!
// 3. 要不要listen? -- 不需要!客戶端不需要建立鏈接
// 4. 要不要accept? -- 不要!
// 5. 要什么? 要發(fā)起鏈接!
}
void start()
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(_serverport);
server.sin_addr.s_addr = inet_addr(_serverip.c_str());
if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0)
{
std::cerr << "socket connect error" << std::endl;
}
else
{
std::string msg;
while (true)
{
std::cout << "Enter# ";
std::getline(std::cin, msg);
write(_sock, msg.c_str(), msg.size());
char buffer[NUM];
int n = read(_sock, buffer, sizeof(buffer) - 1);
if (n > 0)
{
// 目前我們把讀到的數(shù)據(jù)當成字符串, 截至目前
buffer[n] = 0;
std::cout << "Server回顯# " << buffer << std::endl;
}
else
{
break;
}
}
}
}
~TcpClient()
{
if(_sock >= 0) close(_sock); //不寫也行,因為文件描述符的生命周期隨進程,所以進程退了,自然也就會自動回收了
}
private:
int _sock;
std::string _serverip;
uint16_t _serverport;
};
tcpServer.cc
#include "tcpServer.hpp"
#include <memory>
using namespace server;
using namespace std;
static void Usage(string proc)
{
cout << "Usage:\n\t" << proc << " local_port\n\n"; // 命令提示符
}
// tcp服務器,啟動上和udp server一模一樣
// ./tcpserver local_port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<TcpServer> tsvr(new TcpServer(port));
tsvr->initServer();
tsvr->start();
return 0;
}
tcpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "log.hpp"
namespace server
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
static const uint16_t gport = 8080;
static const int gbacklog = 5; // 10、20、50都可以,但是不要太大比如5千,5萬
class TcpServer
{
public:
TcpServer(const uint16_t &port = gport) : _listensock(-1), _port(port)
{
}
void initServer()
{
// 1. 創(chuàng)建socket文件套接字對象 -- 流式套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0); // 第三個參數(shù)默認 0
if (_listensock < 0)
{
logMessage(FATAL, "create socket error");
exit(SOCKET_ERR);
}
logMessage(NORMAL, "create socket success");
// 2.bind綁定自己的網(wǎng)路信息 -- 注意包含頭文件
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port); // 這里有個細節(jié),我們會發(fā)現(xiàn)當我們接受數(shù)據(jù)的時候是不需要主機轉(zhuǎn)網(wǎng)路序列的,因為關(guān)于IO類的接口,內(nèi)部都幫我們實現(xiàn)了這一功能,這里不幫我們做是因為我們傳入的是一個結(jié)構(gòu)體,系統(tǒng)做不到
local.sin_addr.s_addr = INADDR_ANY; // 接受任意ip地址
if (bind(_listensock, (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) -- TCP與UDP不同,它先要建立鏈接之后,TCP是面向鏈接的,后面還會有“握手”過程
if (listen(_listensock, gbacklog) < 0) // 第二個參數(shù)backlog后面再填這個坑
{
logMessage(FATAL, "listen socket error");
exit(LISTEN_ERR);
}
logMessage(NORMAL, "listen socket success");
}
void start()
{
for (;;) // 一個死循環(huán)
{
// 4. server 獲取新鏈接
// sock 和client 進行通信的fd
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
logMessage(ERROR, "accept error, next"); // 這個不影響服務器的運行,用ERROR,就像張三不會因為沒有把人招呼進來就不干了
continue;
}
logMessage(NORMAL, "accept a new link success");
std::cout << "sock: " << sock << std::endl;
// 5. 這里就是一個sock, 未來通信我們就用這個sock, 面向字節(jié)流的,后續(xù)全部都是文件操作!
// 我們就可以直接使用read之類的面向字節(jié)流的操作都行
// version 1
serviceIO(sock);
close(sock); // 走到這里就說明客戶端已經(jīng)關(guān)閉
// 對一個已經(jīng)使用完畢的sock,我們要關(guān)閉這個sock,要不然會導致,文件描述符會越來越少,因為文件描述符本質(zhì)就是一個數(shù)組下標
// 只要是數(shù)組下標就會有盡頭,提供服務的上限 就等于文件描述符的上限
// 對一個已經(jīng)使用完畢的sock,我們要關(guān)閉這個sock,要不然會導致,文件描述符泄漏
}
}
void serviceIO(int sock)
{
// 先用最簡單的,讀取再寫回去
char buffer[1024];
while (true)
{
ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
if (n > 0)
{
// 截至目前,我們把讀到的數(shù)據(jù)當作字符串
buffer[n] = 0;
std::cout << "recv message: " << buffer << std::endl;
std::string outbuffer = buffer;
outbuffer += "server[echo]";
write(sock, outbuffer.c_str(), outbuffer.size()); // 在多路轉(zhuǎn)接的時候再詳談write的返回值
}
else if (n == 0)
{
// 代表client退出 -- 把它想象成一個建立好的管道,客戶端不寫了,并且把它的文件描述符關(guān)了,讀端就會像管道一樣讀到 0 TCP同理
logMessage(NORMAL, "client quit, me too!");
break;
}
}
}
~TcpServer() {}
private:
int _listensock; // 修改二:改為listensock 不是用來進行數(shù)據(jù)通信的,它是用來監(jiān)聽鏈接到來,獲取新鏈接的!
uint16_t _port;
};
} // namespace server
轉(zhuǎn)下文:簡單的TCP網(wǎng)絡程序·多進程、多線程(后端服務器)_文章來源地址http://www.zghlxwxcb.cn/news/detail-501312.html
到了這里,關(guān)于簡單的TCP網(wǎng)絡程序·單進程(后端服務器)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!