目录
一.服务端
与上文介绍的UDP网络通信大概类似,部分函数的接口相同。
1.1创建套接字与绑定
class ServerTcp
{
public:
ServerTcp(uint16_t port, std::string ip = " ")
: ip_(ip), port_(port), listenSock_(-1)
{
}
void init()
{
listenSock_ = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字
if (listenSock_ < 0)
{
std::cerr << "创建套接字失败\n";
exit(-1);
}
std::cout << "创建套接字成功"
<< " "
<< "listenSock_="
<< listenSock_ << std::endl;
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = PF_INET;
local.sin_port = htons(port_);
ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(),
&local.sin_addr));
if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0) // 绑定
{
std::cerr << "绑定失败\n";
exit(-2);
}
std::cout << "绑定成功" << std::endl;
private:
int listenSock_;
std::string ip_;
uint16_t port_;
};
补充内容:
1.如果用的是云服务器,在填充服务器的IP地址时,不需要显示绑定IP地址,直接将IP地址设置为
INADDR_ANY
即可,此时服务器就可以从本地任何一张网卡当中读取数据。2.创建套接字时所需的服务类型应该是
SOCK_STREAM
,在编写TCP网络通信时,SOCK_STREAM
提供的是一个有序的、可靠的、全双工的、基于连接的流式服务。
1.2监听
TCP服务器是面向连接的,客户端向服务器发送数据时,要确保二者已经建立了关联,将套接字设置为监听状态,然后去监听socket。
函数listen:
int listen(int sockfd, int backlog);
参数解释:
sockfd:需要设置为监听状态的套接字对应的文件描述符。
backlog:全连接队列的最大长度。若有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度。
返回值说明:成功返回0,失败返回-1。
if (listen(listenSock_, 5 ) < 0)
{
exit(-2);
}
std::cout << "listen success\n";
1.3服务端获取连接
TCP服务器在与客户端进行网络通信之前,服务器需要先获取到客户端发送的连接请求。
accept函数:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数解释:
返回值说明:其返回值也是一个文件描述符,前面第一次创建的listensock_是用于不断的去获取客户端发来的连接请求,收到连接请求后会再创建一个套接字(也就是其返回值),该套接字(也就是其返回值)用于为本次accept获取到的连接提供服务。
sockfd:特定的监听套接字,表示从该监听套接字中获取连接,进行通信。
addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
addrlen:调用时传入期望读取的addr结构体的长度,返回时代表实际读取到的addr结构体的长度,这是一个输入输出型参数。
代码:
void start()
{
threadpool<Task> *p = threadpool<Task>::getInstance();
p->start();
while (true)
{
struct sockaddr_in peer; //用于获取对端信息
socklen_t len = sizeof(peer);
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
if (serviceSock < 0)
{
continue; // 不断的去获取客户端发送来的请求
}
std::cout << "获取链接成功"
<< " "
<< "servericeSock=" << serviceSock << std::endl;
// 获取客户端基本信息
uint16_t peerPort = ntohs(peer.sin_port);
std::string peerIp = inet_ntoa(peer.sin_addr);
// startserver(serviceSock, peerPort, peerIp); 下面说明
}
1.4服务端提供服务
当服务器与客户端建立关联后,客户端就可以向服务器发送数据了,之后服务器就应该接受这些数据,并作出对应的处理。其实二者建立关联后,该accept函数的返回值就是对应的文件描述符,服务器可以从中读取,发送数据。(上面代码最后一段调用下面函数)
要用到read函数,write函数,这里不在介绍了。
void startserver(int sock, uint16_t peerPort, std::string peerIp)
{
char inbuffer[1024]; //将读入的数据放入inbuffer中
assert(sock > 0 && !peerIp.empty());
while (true)
{
ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1);
if (s > 0)
{
inbuffer[s] = '\0';
std::cout << peerIp << " " << peerPort << " "
<< "client>>" << inbuffer << std::endl;
for (int i = 0; i < s; i++) //将小写字母转为大写,再发送给客户端
{
if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
inbuffer[i] = toupper(inbuffer[i]);
}
write(sock, inbuffer, strlen(inbuffer)); //将数据发送给客户端
}
else if (s == 0)
{
std::cout << peerIp << " " << "clinet quit\n";
break;
}
else
{
std::cerr << "读取错误\n";
break;
}
}
close(sock);
}
在上面代码中,提供服务的函数正常情况下是一个死循环,若是单进程的话,服务器也就一次只能为一个客户端提供服务,这显然是不合理的。服务器也该可以为许多客户端提供服务的。所以该提供服务的函数应该让创建的子进程或者创建的线程去执行。
创建子进程执行任务代码:
当子进程退出时会给父进程发送SIGCHLD信号,若父进程对该进行捕捉,并将该信号的处理动作设置为忽略,那么父进程可以继续执行自己的代码。
signal(SIGCHLD, SIG_IGN);//这只在LInux中有效
pid_t id = fork();
if (id == 0)
{
close(listenSock_);
startserver(serviceSock, peerPort, peerIp);//创建的子进程进行服务
exit(0);
}
close(serviceSock);
也有另外一种写法:当父进程创建子进程后,再让该子进程fork,创建孙子进程,让孙子进程去执行任务代码,后子进程直接退出,父进程可以直接等待子进程。而该孙子进程变成孤儿进程,由操作系统管理,退出后,操作系统会对其进行回收处理。
pid_t id1 = fork();
if (id1 == 0)
{
pid_t id2 = fork();
if (id2 == 0)
{
startserver(serviceSock, peerPort, peerIp);//孙子进程
exit(0);
}
exit(0);//子进程直接退出,孙子进程被bash管理
}
pid_t ret = waitpid(id1, nullptr, 0); //父进程可以直接阻塞式等待
close(serviceSock);
二.客户端
与上文介绍的UDP客户端类似,同样不需要自己主动去绑定。
2.1创建套接字
class ClientTcp
{
public:
ClientTcp(std::string ip, uint16_t port)
: ip_(ip), port_(port), sock_(-1)
{
}
void init()
{
sock_ = socket(AF_INET, SOCK_STREAM, 0);
if (sock_ < 0)
{
std::cerr << "创建套接字失败\n";
exit(-1);
}
std::cout << "创建套接字成功" << " " << "sock_=" << sock_ << std::endl;
}
private:
uint16_t port_;
int sock_;
std::string ip_;
};
2.2客户端获取连接
TCP服务器在与客户端进行网络通信之前,客户端需要向服务器发送连接请求。
connect函数:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数解释:
sockfd:特定的套接字,表示通过该套接字发起连接请求。
addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
addrlen:传入的addr结构体的长度。
返回值说明:成功返回0.失败返回-1。
void init()
{
sock_ = socket(AF_INET, SOCK_STREAM, 0);
if (sock_ < 0)
{
std::cerr << "创建套接字失败\n";
exit(-1);
}
std::cout << "创建套接字成功"
<< " "
<< "sock_=" << sock_ << std::endl;
struct sockaddr_in server; //填充服务端的信息
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(port_);
inet_aton(ip_.c_str(), &server.sin_addr);
// 向服务端发起连接请求
if (connect(sock_, (const struct sockaddr*)&server, sizeof(server)) != 0)
{
std::cerr << "connect: " << strerror(errno) << std::endl;
exit(-1);
}
std::cout << "连接成功\n";
}
2.3客户端发送需求
当客户端发送连接请求,当服务端收到请求后,连接成功后,二者就可以进行通信了。这里模拟客户端向服务器发送数据,后再接受服务器发来的数据。同样是用到read与write函数。
代码:
void start()
{
std::string outbuffer;
std::string inbuffer;
while (true)
{
std::cout << "请输入信息>>";
std::getline(std::cin, outbuffer);
ssize_t s = write(sock_, outbuffer.c_str(), outbuffer.size());
if (s > 0)
{
inbuffer.resize(1024, 0);
ssize_t s = read(sock_, (char*)inbuffer.c_str(), 1024);
if (s > 0)
{
inbuffer[s] = '\0';
std::cout << "server>> " << inbuffer << std::endl;
}
else
break;
}
}
close(sock_);
}
三.填充命令行参数
3.1客户端
int main(int argc, char *argv[])
{
if (argc != 2 && argc != 3)
{
std::cerr << "Usage:\n\t" << argv[0] << " port ip" << std::endl;
std::cerr << "example:\n\t" << argv[0] << " 8080 127.0.0.1 \n"
<< std::endl;
exit(-3);
}
std::string ip;
uint16_t port = atoi(argv[1]);
if (argc == 3)
ip = argv[2];
ServerTcp *T = new ServerTcp(port, ip);
T->init();
T->start();
return 0;
}
3.2服务端
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage:\n\t" << argv[0] << " port ip" << std::endl;
std::cerr << "example:\n\t" << argv[0] << " 127.0.0.1 8080 \n"
<< std::endl;
exit(-3);
}
std::string ip;
uint16_t port = atoi(argv[2]);
ip = argv[1];
ClientTcp *C = new ClientTcp(ip, port);
C->init();
C->start();
return 0;
}
3.3结果测试
四.线程版本服务端
4.1线程版
同样是对该执行任务的函数进行处理。在创建线程对应执行任务的函数时,该函数需要有对应客户端的IP,端口号。所以可以再写一个类来保存这些信息,后用一个指向该类的指针当参数,传给该执行任务的函数即可。
对应保存客户端数据的类:
class pthreadStart
{
public:
pthreadStart(ServerTcp* thi,uint16_t clientPort,std::string clientIP,int sock)
:this_(thi)
,clientPort_(clientPort)
,sock_(sock)
,clinetIp_(clientIP)
{
}
uint16_t clientPort_;//对应客户
std::string clinetIp_;
int sock_;
ServerTcp* this_; //通过该指针调用处理任务函数
};
执行任务函数:该函数在类内,用static修饰,去掉this指针
static void* Routine(void* args)
{
pthread_detach(pthread_self());
pthreadStart* p = static_cast<pthreadStart*>(args);
p->this_->startserver(p->sock_, p->clientPort_, p->clinetIp_);
}
void start()
{
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int serviceSock = accept(listenSock_, (struct sockaddr*)&peer, &len);
if (serviceSock < 0)
{
continue;
}
std::cout << "获取链接成功"
<< " "
<< "servericeSock=" << serviceSock << std::endl;
// 获取客户端基本信息
uint16_t peerPort = ntohs(peer.sin_port);
std::string peerIp = inet_ntoa(peer.sin_addr);
pthread_t tid;
pthreadStart* p = new pthreadStart(this, peerPort, peerIp, serviceSock);
pthread_create(&tid, nullptr, Routine, (void*)p);
}
}
4.2线程池版
封装一个线程的类,把创建线程的函数放入该类内,并提供相应接口。
线程池介绍:Linux中线程池的制作_"派派"的博客-CSDN博客
void start()
{
threadpool<Task>* p = threadpool<Task>::getInstance();//创建一个线程池
p->start(); //线程创建,在等待任务到来
//***************************************************************************
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int serviceSock = accept(listenSock_, (struct sockaddr*)&peer, &len);
if (serviceSock < 0)
{
continue;
}
std::cout << "获取链接成功"
<< " "
<< "servericeSock=" << serviceSock << std::endl;
// 获取客户端基本信息
uint16_t peerPort = ntohs(peer.sin_port);
std::string peerIp = inet_ntoa(peer.sin_addr);
Task t(peerPort,peerIp,serviceSock); //把对应客户端信息放入任务
p->push(t); //线程池加入任务
}
任务代码:暂且把处理任务函数放入任务代码中,每个任务还保存有对应客户端的IP,端口号,以及对应套接字。
class Task
{
public:
Task(uint16_t clientPort, std::string clientIP, int sock)
: sock_(sock), clinetIp_(clientIP), clientPort_(clientPort)
{
}
void startserver(int sock, uint16_t peerPort, std::string peerIp)
{
char inbuffer[1024];
assert(sock > 0 && !peerIp.empty());
while (true)
{
ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1);
if (s > 0)
{
inbuffer[s] = '\0';
std::cout << peerIp << " " << peerPort << " "
<< "client>>" << inbuffer << std::endl;
for (int i = 0; i < s; i++)
{
if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
inbuffer[i] = toupper(inbuffer[i]);
}
write(sock, inbuffer, strlen(inbuffer));
}
else if (s == 0)
{
std::cout << peerIp << " "
<< "clinet quit\n";
break;
}
else
{
std::cerr << "读取错误\n";
break;
}
}
close(sock);
}
void run()
{
std::cout<<"线程:"<<pthread_self()<<"开始处理工作\n";
startserver(sock_,clientPort_,clinetIp_);
std::cout<<"线程:"<<pthread_self()<<"结束工作\n";
}
public:
uint16_t clientPort_;
std::string clinetIp_;
int sock_;
};
线程池代码:当创建的线程去拿到任务后,就去调用任务的处理函数,去处理任务。
template <class T>
class threadpool
{
public:
threadpool(const threadpool<T> &) = delete;
void operator=(const threadpool<T> &) = delete;
static threadpool<T> *getInstance()
{
if (nullptr == instance) // 过滤重复的判断
{
if (nullptr == instance)
{
instance = new threadpool<T>;
}
}
return instance;
}
threadpool(int nums = pthnums)
{
pthread_num = nums;
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&cond_, nullptr);
is_start = true;
}
~threadpool()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&cond_);
}
static void *Routine(void *argv) // 类内函数有this指针,将其设置为静态,argv接受this
{
pthread_detach(pthread_self());
threadpool<T> *tp = static_cast<threadpool<T> *>(argv);
while (true)
{
cout << "pthread[" << pthread_self() << "]running" << endl;
tp->lockQueue();
while (tp->isempty())
{
tp->waitTask();
}
T t = tp->pop(); // 线程拿到任务
tp->unlockQueue();
t.run(); //线程调用执行任务函数
}
}
void start() // 创建pthread_num个线程
{
assert(is_start);
for (int i = 0; i < pthread_num; i++)
{
pthread_t tid;
pthread_create(&tid, nullptr, Routine, this);
}
is_start = false;
}
void push(const T x)
{
lockQueue();
_q.push(x);
unlockQueue();
SignalTask();
}
T pop()
{
T x = _q.front();
_q.pop();
return x;
}
private:
// 封装的接口
void lockQueue()
{
pthread_mutex_lock(&mutex_);
}
void unlockQueue()
{
pthread_mutex_unlock(&mutex_);
}
void waitTask()
{
pthread_cond_wait(&cond_, &mutex_);
}
void SignalTask()
{
pthread_cond_signal(&cond_);
}
bool isempty()
{
return _q.empty();
}
private:
queue<T> _q;
pthread_mutex_t mutex_; // 互斥锁
pthread_cond_t cond_; // 信号量
int pthread_num; // 创建线程数量
bool is_start;
static threadpool<T> *instance;
};
template <class T>
threadpool<T> *threadpool<T>::instance = nullptr;
结果:
改进:其实该处理任务的函数可以放在任务函数中,也可以放入线程池中,但为了代码的解耦性,可其放入server端的代码上,后面执行该函数时,可以通过回调的方法。但最好的方法是将处理任务的代码封装成一个类(仿函数),里面包含具体的处理方法,然后将该类的对象放入任务端(Task)。下面就介绍第一种方法。
例如:
server端代码,将任务处理函数的实现放在服务端,后面传给任务端。
Task t(peerPort,peerIp,serviceSock,startserver);
p->push(t);
任务端代码:
class Task
{
public:
//typedef std::function<void (int, uint16_t,std::string)> callback_t;//类型的声明
using callback_t = std::function<void(int, std::string, uint16_t)>;
Task(uint16_t clientPort, std::string clientIP, int sock, callback_t func)
: sock_(sock), clinetIp_(clientIP), clientPort_(clientPort), func_(func)
{
}
void run()
{
std::cout<<"线程:"<<pthread_self()<<"开始处理工作\n";
func_(sock_,clientPort_,clinetIp_); //进行回调
std::cout<<"线程:"<<pthread_self()<<"结束工作\n";
}
public:
uint16_t clientPort_;
std::string clinetIp_;
int sock_;
callback_t func_; // 回调方法
// void(*p_)(int,uint16_t,std::string);
};