1、什么是监听套接字,和UDP相比,TCP为什么文件描述符变多了?
在网络编程中,TCP和UDP是两种常见的传输协议,它们之间最大的不同之一在于连接的管理方式。为了更好地理解这个区别,我们可以用一个生动的比喻来说明。
假设你在一家餐馆工作,其中张三负责把客人带到座位上,而李四和王五是服务员,他们负责为客人点菜和服务。张三的角色类似于TCP连接中的监听套接字。他的工作是确保当客户到达时,他们能够被快速、有效地分配给一个合适的服务员。
在TCP协议中,监听套接字(就像张三)是独特的:它负责创建监听端口,通过这个端口,服务器可以接收到请求。当有新的连接请求时,该监听套接字的角色是"接客",将请求转交给一个新的文件描述符(类似李四或王五),这个新的文件描述符负责具体的数据传输和处理。因此,虽然张三(监听套接字)数量是固定的,但却需要多个"服务员"来处理多个客户连接,即多个文件描述符。相比之下,UDP协议不需要这种复杂的连接管理机制,因为它是无连接的,不需要维护任何长时间的连接状态,因此它的文件描述符相对少。
2、如果现在还没写客户端,只写了服务器端,怎么测试当前服务器通信的时候会有别人来连我呢?工具:telnet 127.0.0.1 8888(底层默认tcp)
当你正在开发一个服务器应用程序,但还没有编写客户端时,你可能会想测试服务器的连接和通信功能。这时,我们可以使用一个简单的工具------telnet。假设你的服务器正在本地计算机上运行,并监听8888端口,你可以在命令行中输入以下命令来测试连接:
要检查当前活跃的网络连接,可以使用以下命令:
netstat -nltp
服务器端代码写好了,直接用工具来测试。
cpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include "Log.hpp"
#include <string>
#include <arpa/inet.h>
#include <cstring>
#include <netinet/in.h>
const int defaultfd=-1;
const std::string defaultip="0.0.0.0";
const int backlog=10;//一般不要设置的太大,后面解释
Log lg;
enum{
SOCKET_ERR=2,
BIND_ERR,
Listen_ERR
};
class TcpServer
{
public:
TcpServer(const uint16_t &port,const std::string &ip=defaultip):listensock_(defaultfd),port_(port),ip_(ip)
{};
void InitServer()
{
listensock_=socket(AF_INET,SOCK_STREAM,0);
if(listensock_<0)
{
lg(Fatal,"errno:%d,errrstring:%s",errno,strerror(errno));
exit(SOCKET_ERR);
}
lg(Info,"create socket success,listensock_:%d",listensock_);
struct sockaddr_in local; //服务器信息
memset(&local,0,sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons(port_); //用户在构建服务器时就需要告诉我
inet_aton(ip_.c_str(),&(local.sin_addr));
local.sin_addr.s_addr=INADDR_ANY; //或者写成0
if(bind(listensock_,(const sockaddr*)&local,sizeof(local))<0)
{
lg(Fatal,"bind error,errno:%d,errrstring:%s",errno,strerror(errno));
exit(BIND_ERR);
}
lg(Info,"bind success");
//和UDP不一样的地方,设为监听状态 Tcp是面向链接的,服务器一般是比较被动的。服务器一直处于一种,一直在等待连接到来的状态
if(listen(listensock_,backlog)<0)
{
lg(Fatal,"errno:%d,errrstring:%s",errno,strerror(errno));
exit(Listen_ERR);
}
}
void Start()
{
lg(Info, "tcpserver is running....");
while (true)
{
// 1.获取新连接
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);
if (sockfd < 0)
{
lg(Warning, "errno:%d,errrstring:%s", errno, strerror(errno));//获取连接失败,就直接再去获取
continue;
}
//到底是谁连接的我
uint16_t clientport=ntohs(client.sin_port); //网络转主机
char clientip[32];
std::string ip=inet_ntop(AF_INET,&(client.sin_addr),clientip,sizeof(clientip));//把客户端的ip存到ipstr中
//2.根据新连接进行通信
// lg(Info,"get a new Link...,sockfd:%d,client ip:%s,client port:%d",sockfd,clientip,clientport);
//version 1
Service(sockfd,ip,clientport);//拿到客户端的端口和ip
}
}
void Service(int sockfd,const std::string &clientip,uint16_t &clientport)
{
char buffer[4096];
//测试代码:你给我发什么我就给你响应什么
while(true)
{
ssize_t n=read(sockfd,buffer,sizeof(buffer));
if(n>0)
{
buffer[n]=0;
std::cout<<"client say#"<<buffer<<std::endl;
std::string echo_string="tpserver echo#";
echo_string+=buffer;
write(sockfd,echo_string.c_str(),echo_string.size()); //再写回去
}
}
}
~TcpServer()
{};
private:
int listensock_;
uint16_t port_;
std::string ip_;
};
测试:ctrl+] 进入 ctrl+] quit退出
发现客户端发的aaaaa,服务器端收到了。
问题1:要是客户端退出,服务器会怎么办?
服务器会读到0,服务中止,服务器端也直接退出
问题2:服务器向一个不存在的fd写入
signal(SIGPIPE,SIG_IGN);//防止出现写入的时候,向一个已经关闭的文件描述符写入的时候,此时连接没有意义,这时写时,os直接SIGPIPE掉
3、代码
cpp
TcpServer.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include "Log.hpp"
#include <string>
#include <arpa/inet.h>
#include <cstring>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include<sys/wait.h>
#include<pthread.h>
#include "ThreadPool.hpp"
#include"Task.hpp"
#include<signal.h>
const int defaultfd=-1;
const std::string defaultip="0.0.0.0";
const int backlog=10;//一般不要设置的太大,后面解释
Log lg;
enum{
SOCKET_ERR=2,
BIND_ERR,
Listen_ERR
};
class TcpServer;
class ThreadData//把TcpServer本身传进来
{
public:
ThreadData(int fd,const std::string &ip,const uint16_t &port,TcpServer *t):sockfd(fd),clientip(ip),clientport(port),tsvr(t)
{}
public:
int sockfd;
std::string clientip;
uint16_t clientport;
TcpServer *tsvr;
};
class TcpServer
{
public:
TcpServer(const uint16_t &port,const std::string &ip=defaultip):listensock_(defaultfd),port_(port),ip_(ip)
{};
void InitServer()
{
listensock_=socket(AF_INET,SOCK_STREAM,0);
if(listensock_<0)
{
lg(Fatal,"errno:%d,errrstring:%s",errno,strerror(errno));
exit(SOCKET_ERR);
}
lg(Info,"create socket success,listensock_:%d",listensock_);
struct sockaddr_in local; //服务器信息
memset(&local,0,sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons(port_); //用户在构建服务器时就需要告诉我
inet_aton(ip_.c_str(),&(local.sin_addr));
local.sin_addr.s_addr=INADDR_ANY; //或者写成0
if(bind(listensock_,(const sockaddr*)&local,sizeof(local))<0)
{
lg(Fatal,"bind error,errno:%d,errrstring:%s",errno,strerror(errno));
exit(BIND_ERR);
}
lg(Info,"bind success");
//和UDP不一样的地方,设为监听状态 Tcp是面向链接的,服务器一般是比较被动的。服务器一直处于一种,一直在等待连接到来的状态
if(listen(listensock_,backlog)<0)
{
lg(Fatal,"errno:%d,errrstring:%s",errno,strerror(errno));
exit(Listen_ERR);
}
}
// void Service(int sockfd,const std::string &clientip,uint16_t &clientport)
// {
// char buffer[4096];
// //测试代码:你给我发什么我就给你响应什么
// while(true)
// {
// ssize_t n=read(sockfd,buffer,sizeof(buffer));
// if(n>0)
// {
// buffer[n]=0;
// std::cout<<"client say#"<<buffer<<std::endl;
// std::string echo_string="tpserver echo#";
// echo_string+=buffer;
// write(sockfd,echo_string.c_str(),echo_string.size()); //再写回去
// }
// else if(n==0)
// {
// //lg(Info,"%s:%d quit,server close sockfd:%d",clientip,clientport,sockfd);
// break;
// }
// else
// {
// lg(Warning,"read Error...,sockfd:%d,client ip:%s,client port:%d",sockfd,clientip.c_str(),clientport);
// }
// }
// }
// static void* routine(void *args)//静态函数,把this传进来
// {
// pthread_detach(pthread_self());//把自己设置为分离状态,主线程一直再获取新连接,创建出线程就不管了,让新线程进行任务处理
// ThreadData *td=static_cast<ThreadData *>(args);
// td->tsvr->Service(td->sockfd,td->clientip,td->clientport);//由该线程提供服务
// delete td;//提供完服务,申请的堆空间释放掉
// return nullptr;
// }
void Start()
{
signal(SIGPIPE,SIG_IGN);//防止出现写入的时候,向一个已经关闭的文件描述符写入的时候,此时连接没有意义,这时写时,os直接SIGPIPE掉
ThreadPool<Task>::GetInstance()->Start();//启动线程池
lg(Info, "tcpserver is running....");
while (true)
{
// 1.获取新连接
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);
if (sockfd < 0)
{
lg(Warning, "errno:%d,errrstring:%s", errno, strerror(errno));//获取连接失败,就直接再去获取
continue;
}
//到底是谁连接的我
uint16_t clientport=ntohs(client.sin_port); //网络转主机
char clientip[32];
inet_ntop(AF_INET,&(client.sin_addr),clientip,sizeof(clientip));//把客户端的ip存到ipstr中
//2.根据新连接进行通信
//lg(Info,"get a new Link...,sockfd:%d,client ip:%s,client port:%d",sockfd,ip,clientport);
//version 1
// Service(sockfd,ip,clientport);//拿到客户端的端口和ip
// close(sockfd);
//version 2 --多进程版,因为创建一个进程成本太高了
// pid_t id=fork();
// if(id==0)
// {
// close(listensock_);
// if(fork()>0) exit(0);
// //子进程 什么都可以看到
// Service(sockfd,ip,clientport);//孙子进程,wait立马返回,父进程和孙子进程并发访问,不用管孙子,不用等孙子,儿子已经挂了,孙子会被系统领养
// close(sockfd);
// exit(0);
// }
// close(sockfd);
// //父进程继续获取新连接 打开文件描述符,打开之后交给子进程,自己就关掉了,如果不关,系统中会有非常多的文件没有关闭
// pid_t rid=waitpid(id,nullptr,0);
// (void)rid;
//version 3 多线程版本
// ThreadData *td=new ThreadData(sockfd,ip,clientport,this);//this把当前对象传进来
// pthread_t tid;
// pthread_create(&tid,nullptr,routine,td);
//version 4 线程池版
Task t(sockfd,clientip,clientport);
ThreadPool<Task>::GetInstance()->Push(t);
}
}
~TcpServer()
{};
private:
int listensock_;
uint16_t port_;
std::string ip_;
};
cpp
ThreadPool.hpp
#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;
cpp
TcpClient.cc
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
//./tcpclient serverip serverport
void Usage(const std::string &proc)
{
std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr)); // 字符串转为in_addr
while (true) // 每次翻译的时候都要重新建立连接,因为服务器每次只给我提供一次服务
{
int cnt = 5;
int isreconnect = false;
int sockfd = 0;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return 1;
}
do
{
// tcp客户端要不要bind,要不要显示的bind?
// 客户端需要绑定,不需要显示的绑定,将来再通信时需要端口号和ip标识自己的唯一性,当发出消息的时候,服务器才能把消息转过来,但是对于客户端来说
// 端口号具体是几不重要,唯一就行,由os随机选择,根据你的需要随机选中,UDP首次发送数据的时候
// 客户端向服务器发起连接,TCP中客户端发起connect时,进行自动随机bind,这个函数最后几个参数是要知道服务器的ip
int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
std::cerr << "connect error" << std::endl;
isreconnect=true;
cnt--;
return 2;
}
else{
break;
}
}while(cnt&&isreconnect);
if(cnt==0)
{
std::cerr<<"user offline..."<<std::endl;
break;
}
//上面的while循环是连接,下面的while是提供服务
// while(true)
// {
std::string message;
// 连接成功,可以直接发消息
std::cout << "Please enter@" << std::endl;
std::getline(std::cin, message);
int n = write(sockfd, message.c_str(), message.size());
if (n < 0)
{
std::cerr << "write error" << std::endl;
break;
}
// 收到服务器消息
char inbuffer[4096];
n = read(sockfd, inbuffer, sizeof(inbuffer));
if (n > 0)
{
inbuffer[n] = 0;
std::cout << inbuffer << std::endl;
break;
}
close(sockfd);
}
// }
return 0;
}
cpp
Main.cc
#include"TcpServer.hpp"
#include<iostream>
#include<memory>
#include <pthread.h>
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
};
//./tcpserver 8080
int main(int argc,char** argv)
{
if(argc!=2)
{
Usage(argv[0]);
exit(1);
}
uint16_t port=std::stoi(argv[1]);
std::unique_ptr<TcpServer>tcp_svr(new TcpServer(port));
tcp_svr->InitServer();
tcp_svr->Start();
return 0;
}
cpp
Task.hpp
#pragma once
#include <iostream>
#include <string>
#include<unistd.h>
#include <unordered_map>
#include"Log.hpp"
#include "Init.hpp"
extern Log lg;
Init init;
class Task
{
public:
Task(int sockfd, const std::string &clientip, uint16_t &clientport) : sockfd_(sockfd), clientip_(clientip), clientport_(clientport)
{
}
void run()
{
char buffer[4096];
ssize_t n = read(sockfd_, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
std::cout << "client key#" << buffer << std::endl;
std::string echo_string=init.translation(buffer);
write(sockfd_, echo_string.c_str(), echo_string.size()); // 再写回去
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,clientport,sockfd);
//break;
}
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_;
};
cpp
Log.hpp
#pragma once
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
printMethod = Screen;
path = "./log/";
}
void Enable(int method)
{
printMethod = method;
}
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
void printLog(int level, const std::string &logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
if (fd < 0)
return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = LogFile;
filename += ".";
filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"
printOneFile(filename, logtxt);
}
~Log()
{
}
void operator()(int level, const char *format, ...)
{
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
// 格式:默认部分+自定义部分
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
// printf("%s", logtxt); // 暂时打印
printLog(level, logtxt);
}
private:
int printMethod;
std::string path;
};
cpp
Init.hpp
#pragma once
#include<iostream>
#include<string>
#include <unordered_map>
#include <fstream>
#include "Log.hpp"
extern Log lg;
const std::string dictname="./dict.txt";
const std::string sep=":";
//apple:苹果
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);//从pos到结尾
}
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;
};
cpp
Makefile
.PHONY:all
all:tcpserver tcpclient
tcpserver:Main.cc
g++ -o $@ $^ -std=c++11 -lpthread
tcpclient:TcpClient.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f tcpserver tcpclient
4、理解前台进程和后台进程
在操作系统中,程序可以在两种模式下运行:前台 和后台。
4.1什么叫前台进程?
前台进程是与用户直接交互的进程。当你在终端中运行程序时(例如,执行./process
),它默认是在前台运行,这意味着它会占用终端并接收来自键盘的输入。这种进程会阻塞终端输入,直到进程结束,所以在它运行期间,像ls
或pwd
这样的命令通常不会有反应。
下面是一个简单的C++程序运行在前台的示例:
cpp
process.cc
#include<iostream>
#include<string>
#include<unistd.h>
int main()
{
while(true)
{
std::cout<<"hello..."<<std::endl;
sleep(1);
}
return 0;
}
4.2后台进程
有时,你可能希望程序在后台运行,以便不阻塞你的终端。这时你可以在命令后面加上符号&
,例如:
bash
./process &
这样,程序就会在后台运行,你可以继续使用终端执行其他命令。
4.3切换前后台
如果需要将一个后台进程切换到前台,可以使用fg
命令。例如fg 1
会将后台任务列表中的第一个任务移至前台。 把后台进程提到前台 fg 1
所以什么叫做前台,什么叫后台?
谁拥有键盘文件