Linux网络--2.2、TCP接口
目录
一、目的
二、封装服务器端
2.1创建服务器类
2.2创建服务器socket
2.3绑定ip与port
2.4启动监听
2.5监听成功接收客户端
2.6停止服务器
三、客户端
四、不同版本处理任务--服务端
4.0任务
4.1单进程
4.2多进程
4.3多线程
4.4线程池
一、目的
1、了解服务端TCP接口的基本步骤并封装为类
2、了解客户端TCO接口的基本步骤
3、对比不同版本的服务端(多进程,多线程.....)
TCP:
二、封装服务器端
2.1创建服务器类
与UDP类似,我们的服务器启动后就不再关闭,我们提供两个接口:
start
:启动服务器stop
:停止服务器
class TcpServer
{
public:TcpServer(){}// 启动服务器void start(){}// 停止服务器void stop(){}~TcpServer(){}
};
2.2创建服务器socket
利用socket创建套接字,与UDP相似,只是在
socket
接口的第二个参数使用SOCK_STREAM
而不再是SOCK_DGRAM,表示面向字节流
void InitServer(){ // 1. 创建tcp socket_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0); // Tcp Socketif (_listensockfd < 0){ LOG(LogLevel::FATAL) << "socket error";Die(SOCKET_ERR);} LOG(LogLevel::INFO) << "socket create success, sockfd is : " << _listensockfd;}
2.3绑定ip与port
绑定方式与UDP基本一致,先使用原生的方式而不是直接使用封装后的
sockaddr_in
结构。在UDP编程接口基本使用部分已经提到过服务器不需要指定IP地址
#include <sys/types.h> /* See NOTES */#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);#define CONV(v) (struct sockaddr*) (v) //类型转换 //填充服务端的网络信息struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(gport);local.sin_addr.s_addr = INADDR_ANY;//接收所有ip// 2. bindint n = ::bind(_listensockfd, CONV(&local), sizeof(local));if (n < 0){ LOG(LogLevel::FATAL) << "bind error";Die(BIND_ERR);} LOG(LogLevel::INFO) << "bind success, sockfd is : " << _listensockfd;
2.4启动监听
由于tcp是面向连接的所以只有监听连接的到来,并在服务器启动后交给accep才能处理
int listen(int sockfd, int backlog);
该接口的第一个参数表示当前需要作为传输的套接字,第二个参数表示等待中的客户端的最大个数。之所以会有第二个参数是因为一旦请求连接的客户端太多但是服务器又无法快速得做出响应就会导致用户一直处于等待连接状态从而造成不必要的损失。一般情况下第二个参数不建议设置比较大,而是因为应该根据实际情况决定,但是一定不能为0,本次大小定为8当监听成功,该接口会返回0,否则返回-1并设置对应的错误码// 3. cs,tcp是面向连接的,就要求tcp随时随地等待被连接 // tcp 需要将socket设置成为监听状态 n = ::listen(_listensockfd, BACKLOG); if (n < 0) {LOG(LogLevel::FATAL) << "listen error";Die(LISTEN_ERR);}LOG(LogLevel::INFO) << "listen success, sockfd is : " << _listensockfd;
2.5监听成功接收客户端
在TCP中,启动服务器的逻辑和UDP的逻辑有一点不同,因为TCP服务器在启动之前先要进行监听,所以实际上此时服务器并没有进入IO状态,所以一旦启动服务器后,首先要做的就是一旦成功建立连接就需要进入收发消息的状态
首先判断服务器是否启动,如果服务器本身已经启动就不需要再次启动,所以还是使用一个_isRunning变量作为判断条件,基本逻辑如下:
// 启动服务器
void start()
{
if (!_isRunning)
{
_isRunning = true;
while (true)
{
}
}
}
接着就是在监听成功的情况下进入IO状态,这里使用的接口就是accept,其原型如下
int accept(int sockfd, struct sockaddr * addr, socklen_t * addrlen);
该接口的第一个参数表示需要绑定的服务器套接字,第二个参数表示对方的套接字结构,第二个参数表示对方套接字结构的大小,其中第二个参数和第三个参数均为输出型参数
需要注意的是该接口的返回值,当函数执行成功时,该接口会返回一个套接字,这个套接字与前面通过socket接口获取到的套接字不同。
在UDP中,只有一个套接字,就是socket的返回值,但是在TCP中,因为首先需要先监听,此时需要用到的实际上是监听套接字,一旦监听成功,才会给定用于IO的套接字。所以实际上,在TCP中,socket接口的返回值对应的是listen用的套接字,而accept的套接字就是用于IO的套接字
void Start(){_isrunning = true;while (_isrunning){// 不能直接读取数据// 1. 获取新连接struct sockaddr_in peer;socklen_t peerlen = sizeof(peer); // 这个地方一定要注意设置peerlen的大小,要不然,会有问题!LOG(LogLevel::DEBUG) << "accept ing ...";// 我们要获取client的信息:数据(sockfd)+client socket信息(accept || recvfrom)int sockfd = ::accept(_listensockfd, CONV(&peer), &peerlen);if (sockfd < 0){LOG(LogLevel::WARNING) << "accept error: " << strerror(errno);continue;}// 获取连接成功了LOG(LogLevel::INFO) << "accept success, sockfd is : " << sockfd;}
2.6停止服务器
// 停止服务器
void stop()
{if (_isRunning){close(_listen_socketfd);close(_ac_socketfd);}
}
三、客户端
与UDP一致,我们不再封装,直接显示的创建套接字,填充服务器信息后与服务器建立连接,建立连接时会自动绑定ip与port,原因与UDP一致
因为tcp面向字节流,与文件类似,所以发送和接收文件可以用read和write,但不保险,可以使用recv和send
// 形式:./client_tcp server_ip server_port11 int main(int argc, char *argv[])12 {13 //启动程序形式warning14 if (argc != 3)15 {16 std::cout << "Usage:./client_tcp server_ip server_port" << std::endl;17 return 1;18 }19 20 //服务端IP21 std::string server_ip = argv[1]; // "192.168.1.1"22 23 //服务端端口号24 int server_port = std::stoi(argv[2]);25 26 //创建套接字,AF_INET--网络,SOCK_STREAM--字节流27 int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);28 if (sockfd < 0)29 {30 std::cout << "create socket failed" << std::endl;31 return 2;32 } 33 34 //服务端的套接字信息初始化,网络转主机35 struct sockaddr_in server_addr;36 memset(&server_addr, 0, sizeof(server_addr));37 server_addr.sin_family = AF_INET;38 server_addr.sin_port = htons(server_port);39 server_addr.sin_addr.s_addr = inet_addr(server_ip.c_str());
// client 不需要显示的进行bind, tcp是面向连接的, connect 底层会自动进行bind42 int n = ::connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));43 if(n < 0)44 {45 std::cout << "connect failed" << std::endl;46 return 3;47 }48 49 // echo client50 std::string message;51 while(true)52 {53 char inbuffer[1024];54 std::cout << "input message: ";55 std::getline(std::cin, message);56 57 n = ::write(sockfd, message.c_str(), message.size());58 if(n > 0)59 {60 int m = ::read(sockfd, inbuffer, sizeof(inbuffer));61 if(m > 0)62 {63 inbuffer[m] = 0;64 std::cout << inbuffer << std::endl;65 }66 else67 break;68 }69 else70 break;71 }::close(sockfd);74 return 0;75 }
四、不同版本处理任务--服务端
4.0任务
任务:利用传入的sockfd(文件fd)接收客户端的消息,成功后并回显消息给客户端
void HandlerRequest(int sockfd) // TCP也是全双工通信的(能收能发){ LOG(LogLevel::INFO) << "HandlerRequest, sockfd is : " << sockfd;char inbuffer[4096];// 长任务while (true){ // 约定:用户发过来的是一个完整的命令string// ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) - 1); // read读取是不完善,可能少字节,用recvssize_t n = ::recv(sockfd, inbuffer, sizeof(inbuffer) - 1, 0); if (n > 0){ LOG(LogLevel::INFO) << inbuffer;//接收并显示客户端消息//构建回显消息std::string echo_str = "server echo# ";echo_str += inbuffer;::send(sockfd,echo_str.c_str(), echo_str.size(), 0); } else if (n == 0){ // read 如果读取返回值是0,表示client退出LOG(LogLevel::INFO) << "client quit: " << sockfd;break;} else{// 读取失败了break;}}::close(sockfd); // fd泄漏问题!}
4.1单进程
只能接收一条连接,用于测试,不符合生活
直接调用
// version-0
// HandlerRequest(sockfd);
4.2多进程
利用子进程创建孙子进程执行任务后子进程直接退,孙子进程被系统领养,解放父进程继续接收连接,实现多连接
类似管道,我们可以关闭父子进程不需要的读写端,防止错误操作
// version-1: (多进程版本)// pid_t id = fork();// if (id == 0)// {// // child// // 问题1: 子进程继承父进程的文件描述符表。两张,父子各一张// // 1. 关闭不需要的fd// ::close(_listensockfd);// if(fork() > 0) exit(0); //子进程退出// // 孙子进程 -> 孤儿进程 -> 1// HandlerRequest(sockfd);// exit(0);// }// ::close(sockfd);// // 不会阻塞// int rid = ::waitpid(id, nullptr, 0);// if(rid < 0)// {// LOG(LogLevel::WARNING) << "waitpid error";// }
4.3多线程
由于多线程共享同一张表我们不能随便关闭读写端
为了解决创建线程传参的参数不符合void*(void*)的问题,我们可以传结构体,进行任务调用
//结构体用于多线程版本创建线程后时参数不匹配问题,多了一个this指针参数struct ThreadData{int sockfd;TcpServer *self;//this指针调用任务处理的类内函数};//用于多线程传递描述符时的覆盖问题,【参数、返回值需要void*】,不加static会多一个this指针参数,利用结构体解决全局后访问类内函数异常的问题static void *ThreadEntry(void *args){pthread_detach(pthread_self());ThreadData *data = (ThreadData *)args;data->self->HandlerRequest(data->sockfd);return nullptr;}// version-2: 多线程版本// 主线程和新线程是如何看待:文件描述符表,共享一张文件描述符表!!// pthread_t tid;// ThreadData *data = new ThreadData;// data->sockfd = sockfd;// data->self = this;// pthread_create(&tid, nullptr, ThreadEntry, data);
4.4线程池
利用bind或者lambda表达式进行任务执行,线程池为我们封装的单例线程池模式,可以进行任务插入
// version-3:线程池版本 比较适合处理短任务,或者是用户量少的情况// task_t f = std::bind(&TcpServer::HandlerRequest, this, sockfd); // 构建任务
ThreadPool<task_t>::getInstance()->Equeue([this, sockfd](){ this->HandlerRequest(sockfd); });