1️⃣ 在linux下,通过套接字实现服务器和客户端的通信。
2️⃣ 实现单线程、多线程通信。或者实现线程池来通信。
3️⃣ 优化通信,增加守护进程。
有情提醒,类里面默认的函数是内联。内联函数在调用的地方展开,没有函数地址,无法保持和传递。
一 、
1.1实现TCP服务器
创建socket文件描述符(TCP/UDP,客户端+服务端)
int socket(int domain, int type, int protocol);
绑定端口号(TCP/UDP,服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端
int connect(int sockfd, const struct sockaddr*addr,socklen_t addrlen);
void init()
{
// 1.创建套接字
// 1.1 协议族 1.2 SOCK_STREAM是一个有序、可靠、面向连接 的双向字节流 1.3建立一个流式套接字
listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
if (listenSock_ < 0)
{
logMessage(FATAL, "socket: %s", strerror(errno));
exit(SOCKET_ERR);
}
logMessage(DEBUG, "socket: %s,%d", strerror(errno), listenSock_);
// 2.bind绑定
// 2.1填充服务器信息
// sockaddr_in是系统封装的一个结构体,
// 具体包含了成员变量:sin_family、sin_addr、sin_zero
struct sockaddr_in local; // 用户栈
memset(&local, 0, sizeof local);
local.sin_family = PF_INET;
local.sin_port = htons(port_);
// 绑定INADDR_ANY,管理一个套接字不管数据是从哪个网卡过来的,只要是绑定的端口号过来的数据,都可以接收到。
// 将一个网络字节序的IP地址(也就是结构体in_addr类型变量)转化为点分十进制的IP地址(字符串)。
ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
// 2.2本地socket信息,写入sock_对应的内核区域
if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0) // 说明绑定错误
{
logMessage(FATAL, "bind :%s", strerror(errno));
exit(BIND_ERR);
}
logMessage(DEBUG, "bind :%s,%d", strerror(errno), listenSock_);
// 3.监听socket
if (listen(listenSock_, 5) < 0)
{
logMessage(FATAL, "listen :%s", strerror(errno));
exit(LISTEN_ERR);
}
logMessage(DEBUG, "listen :%s,%d", strerror(errno), listenSock_);
// 4.启动线程池
tp_ = ThreadPool<Task>::getInstance();
}
void loop()
{
// 启动线程池
tp_->start();
logMessage(DEBUG, "thread pool start success ,thread number : %d", tp_->ThreadNum());
// signal(SIGCHLD,SIG_IGN);//基类忽略子类信号
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 4.获取连接,accept的返回值是一个新的socket
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
if (serviceSock < 0)
{
logMessage(WARINING, "accept :%s[%d]", strerror(errno), serviceSock);
continue;
}
// 4.1获取客户端基本信息
uint16_t peerPort = ntohs(peer.sin_port); // 将一个16位数由网络字节顺序转换为主机字节顺序
std::string peerIp = inet_ntoa(peer.sin_addr);
logMessage(DEBUG, "accept :%s | %s[%d],socket fd: %d", strerror(errno), peerIp.c_str(), peerPort, serviceSock);
// 5.3 版本 --线程池版本
// 5.3.1 构建任务
// Task t(serviceSock, peerIp, peerPort, std::bind(&ServerTcp::transService, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
// tp_->push(t);//将获取的任务给线程池,目前线程池只有5个线程
// 5.3.2
// 直接使用回调函数
Task t(serviceSock, peerIp, peerPort, execCommand);
// Task::callback_t call = execCommand;
tp_->push(t);
// popen传入命令字符串,让它能以文件的方式读到命令接口
}
}
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
sockfd,利用系统调用socket()建立的套接字描述符,通过bind()绑定到一个本地地址(一般为服务器的套接字),并且通过listen()一直在监听连接;
addr,指向struct sockaddr的指针,该结构用通讯层服务器对等套接字的地址(一般为客户端地址)填写,返回地址addr的确切格式由套接字的地址类别(比如TCP或UDP)决定;若addr为NULL,没有有效地址填写,这种情况下,addrlen也不使用,应该置为NULL;
addrlen,一个值结果参数,调用函数必须初始化为包含addr所指向结构大小的数值,函数返回时包含对等地址(一般为服务器地址)的实际数值;
上边绑定的时候需要将16位主机字节序转16位网络字节序。还有设置=cockaddr_in的结构体里面有域、协议家族该结构体是系统封装的。就是处理网络字节和主机字节的相互转换。获取连接的时候需要重新将网络字节序转化为主机字节序。accept返回的是一个新的socket
1.2实现UDP客户端
创建socket套接字
connect向服务器发起链接请求
int main(int argc,char *argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
// ./clientTcp serverIp serverPort
//默认argc为1,argv[0]为程序名称。如果输入一个参数,则argc为2
std::string serverIp=argv[1];
uint16_t serverPort=atoi(argv[2]);
//1.创建soket
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
std::cerr<<"socket:"<<strerror(errno)<<std::endl;
exit(SOCKET_ERR);
}
//2.CONNECT向服务器发起链接请求
//2.1先填充需要连接的远端主机的基本信息
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_port=htons(serverPort);//将一个16位主机字节顺序转换成网络字节顺序
inet_aton(serverIp.c_str(),&server.sin_addr);//将一个网络字节序的IP地址(也就是结构体in_addr类型变量)转化为点分十进制的IP地址(字符串)。
//2.2发起请求connect自动绑定bind
if(connect(sock,(const struct sockaddr *)&server,sizeof(server))!=0)
{
std::cerr<<"connect:"<<strerror(errno)<<std::endl;
exit(CONN_ERR);
}
std::cout<<"inof : connect success: "<<sock<<std::endl;
std::string message;
while (!quit)
{
message.clear();
std::cout<<"请输入你的消息>>>";
std::getline(std::cin,message);
if(strcasecmp(message.c_str(),"quit")==0)
{
quit=true;
}
size_t s=write(sock,message.c_str(),message.size());
if(s>0)
{
message.resize(1024);
ssize_t s=read(sock,(char *)(message.c_str()),1024);
if(s>0)
message[s]=0;
std::cout<<"Server Echo>>>"<<message<<std::endl;
}
else if(s<=0)
{
break;
}
}
close(sock);
return 0;
}
以上代码主要是创建套接字,获取远端信息,端口和ip将主机字节顺序转换为网络字节顺序。使用connect请求连接。
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
第一个参数:int sockdf:
socket文件描述符
第二个参数: const struct sockaddr *addr:
传入参数,指定服务器端地址信息,含IP地址和端口号
第三个参数:socklen_t addrlen:
传入参数,传入sizeof(addr)大小
返回值:
成功: 0
失败:-1,设置errno
connect()函数返回后并不进行数据交换。而是要等服务器端 accept 之后才能进行数据交换(read、write)。客户端端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。
二、
我们这边要注意既然是实现服务器与客户端互通。我们提供服务就需要考虑让多人连接单人连接等问题。
以下我们实现单线程和多线程版本。
提供服务:
void transService(int sock,const std::string &clientIp,uint16_t clientPort)
{
assert(sock>=0);
assert(!clientIp.empty());
assert(clientPort>=1024);
char inbuffer[BUFFER_SIZE];
while (true)
{
size_t s=read(sock,inbuffer,sizeof(inbuffer)-1);
if(s>0)
{
inbuffer[s]='\0';
if(strcasecmp(inbuffer,"quit")==0)
{
logMessage(DEBUG,"client quit -- %s[%d]",clientIp.c_str(),clientPort);
break;
}
logMessage(DEBUG,"trans before: %s[%d]>>>%s",clientIp.c_str(),clientPort,inbuffer);
//大小写转换
for(int i = 0;i<s;i++)
{
if(isalpha(inbuffer[i])&&islower(inbuffer[i]))
{
inbuffer[i]=toupper(inbuffer[i]);
}
}
logMessage(DEBUG,"trans after : %s[%d]>>> %s",clientIp.c_str(),clientPort,inbuffer);
write(sock,inbuffer,strlen(inbuffer));
}
else if(s==0)
{
logMessage(DEBUG,"client quit -- %s[%d]",clientIp.c_str(),clientPort);
break;
}
else
{
logMessage(DEBUG,"%s[%d] - read:%s",clientIp.c_str(),clientPort,strerror(errno));
break;
}
}
close(sock);
logMessage(DEBUG,"server close %d done",sock);
}
单线程版本:
直接传套接字、ip、端口号
```cpp
transService(serviceSock,peerIp,peerPort);
多线程版本:
//5.1 多线程版本
pid_t id=fork();
if(id==0)
{ //基类进程
close(listenSock_);
//又进行了一次fork,让
if(fork()>0) exit(0);
//子类进程 基类退出--子类还在被系统拿到--回收问题就交给系统回收
transService(serviceSock,peerIp,peerPort);
exit(0);//子进程退出后会进入僵尸状态
}
//基类退出
close(serviceSock);
//获取基类退出后的退出码
pid_t ret=waitpid(id,nullptr,0);
assert(ret>0);
(void)ret;
//多线程会共享文件描述符表
ThreadData td=new ThreadData(peerPort,peerIp,serviceSock,this);
pthread_t tid;
pthread_create(&tid,nullptr,threadRoutine,(void)td);
pthread_detach的使用让每个线程结束后自动释放自己所用的资源。
int serviceSock=accept(listenSock_,(struct sockaddr*)&peer,&len);每个用户连接后需要执行5步。
- ThreadData *td=new ThreadData(peerPort,peerIp,serviceSock,this);这步是为了将主线程把链接信息封装好,然后创建子线程,让子线程去处理这份数据,然后等待下一个链接。
- pthread_create就是完成第一行的创建子进程是执行子进程对应的任务。
线程池版本:
pthread_mutex_init的使用是为了解决在多线程访问共享资源时,多个线程同时对共享资源操作产生的冲突而提出的一种解决方法,在执行时,哪个线程持有互斥锁,并对共享资源进行加锁后,才能对共享资源进行操作,此时其它线程不能对共享资源进行操作。
我们先启动线程池
tp_ = ThreadPool<Task>::getInstance();为每个线程池进行加锁
然后启动线程池
tp_->start();
来了任务就将任务放至任务队列里面,如果没有任务就等待。如果有任务就拿任务并且把任务队列的首部弹出该任务。返回任务。
Task t(serviceSock, peerIp, peerPort, execCommand);
tp_->push(t);
调用程序执和被调用函数同时在执行。当被调函数执行完毕后,被调函数会反过来调用某个事先指定函数,以通知调用程序:函数调用结束。这个过程称为回调(Callback)。
void execCommand(int sock, const std::string& clientIp, uint16_t clientPort)
{
assert(sock >= 0);
assert(!clientIp.empty());
assert(clientPort >= 1024);
char command[BUFFER_SIZE];
while (true)
{
size_t s = read(sock, command, sizeof(command) - 1);
if (s > 0)
{
command[s] = '\0';
// 这个用户执行command命令
logMessage(DEBUG, "[%s:%d] exec [%s] ..done", clientIp.c_str(), clientPort, command);
// 考虑安全
std::string safe = command;
if ((std::string::npos != safe.find("rm")) || (std::string::npos != safe.find("unlink")))
{
break;
}
FILE *fp = popen(command, "r");
if (fp == nullptr)
{
logMessage(WARINING, "exec %s failed,beacuse: %s", command, strerror(errno));
break;
}
// 遍历目录
char line[1024];
while (fgets(line, sizeof(line) - 1, fp) != nullptr)
{
write(sock, line, strlen(line));
}
// //本来要写入dile里但现在写入网络。
// dup2(sock,fp->_fileno);
// fflush(fp);
pclose(fp);
logMessage(DEBUG, "[%s:%d] exec [%s]...done", clientIp.c_str(), clientPort, command);
}
else if (s == 0)
{
logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
break;
}
else
{
logMessage(DEBUG, "%s[%d] - read:%s", clientIp.c_str(), clientPort, strerror(errno));
break;
}
}
close(sock);
logMessage(DEBUG, "server close %d done", sock);
}
对于线程池我的理解还是不行。希望能多多与大家一块交流。