前面我们提到,TCP是要提供可靠的通信,那么其所要做的工作就要比UDP要多的多了,下面我们先直接进行代码编写。
首先不论是TCP还是UDP,对于我们的服务端,都不需要显示的bindIP,只需要一个端口号即可。
对于IP地址,是使用随机绑定的方式。
下面我们对服务端进行编写
EchoTcpServer.hpp

首先就是其服务端的基本结构和我们UDP的一样的,就是一个初始化还有一个启动接口。
下面我们完成对初始化接口的编写:
首先,和UDP一样,我们得先创建套接字socket,不过和UDP不一样的是,我们的TCP中的传输方式要选择字节流的方式。

可以看到上面从建立socket到bind网络信息,和UDP是基本一样的。
但是要补充的是,我们的TCP是要面向连接的,那么我们的TCP服务器就要其处于一种listen监听状态。
那么就要使用下面这个接口:

其作用是对一个socket进行监测,让这个socket进行等待新链接到来。
其第一个参数:就是我们前面所创建的socket。
然后其返回值:
成功,那么就返回0,失败就返回-1。
就好比饭店的老板,其是一直在店里等待顾客的。
其第二个参数:
就是设置我们链接队列的长度。一般我们设置为16或者32。


可以看到有个端口号为8080的tcp服务。
如下是对服务端的初始化代码:
void InitServer()
{
// 创建soclet
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "creata socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::DEBUG) << "create socket success"<<_sockfd;
// 填充网络信息
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY; // 任意IP地址bind
// bind
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel::DEBUG)<<"create bind success";
// tcp是面向连接的,所以TCP服务器要处于一种listen,监听状态
int m = listen(_sockfd, gbacklog);
if (m < 0)
{
LOG(LogLevel::FATAL) << "lisent error";
exit(LISENT_ERR);
}
LOG(LogLevel::DEBUG)<<"create lisent success";
}
下面我们来完成启动的接口,对于TCP的启动,我们不能直接和前面UDP的一样,一上来就开始发送信息,而是要先建立连接,然后才可以进行通信。
那么我们建立连接,要使用下面这个接口:

首先其第一个参数,就是前面我们在初始化的时候创建的socket。
而后面两个参数,就是帮我们获取客户端的socket的IP和port信息。
然后其返回值:
如果其连接成功,那么其会给我们返回一个文件描述符,那么我们前面创建的sockfd和这个文件描述符有何区别?
前面我们所创建是socket就相当于一个中介,其是将我们给这个新连接的文件描述符进行链接的,后续进行网络通信,就和这个accept所返回的文件描述符进行通信,那么我们也称为IOsocket。
也就是服务socket。
未来一个服务socket,那么就代表一条连接。

那么我们建立好了链接,那么就要进行处理了。
在我们的TCP中,我们可以使用和文件读取一样的接口进行读取操作。
当我们在进行读的时候,当读取到的数据长度为0,那么并不是出错,而是说对方不想和我们进行通信了,那么此时我们就可以退出循环,那么就可以断开连接,那么断开连接就是将我们的文件描述符关闭即可。
那么如果读出错,那么也不需要马上终止进程,一样只需要先将其连接终止掉先。
代码如下:
void serverIO(int soclfd, InetAddr &address)
{
LOG(LogLevel::DEBUG) << "client info is:" << address.ToString();
while (true)
{
char inbuffer[1024] = {0};
// 先读取数据
ssize_t n=read(soclfd,inbuffer,sizeof(inbuffer)-1);
if(n>0)
{
inbuffer[n]=0;
LOG(LogLevel::INFO)<<address.ToString()<<"sya#"<<inbuffer;
//写回去给客户端
std::string echo_string ="server echo#";
echo_string+=inbuffer;
write(sockfd,echo_string.c_str(),echo_string.size());
}
else if(n==0)
{
LOG(LogLevel::INFO)<<"client quit,address:"<<address.ToString();
break;
}
else
{
LOG(LogLevel::ERROR)<<"client read error ,address:"<<address.ToString();
break;
}
}
}
如上就是我们服务端的基本代码,下面我们完成对用户端进行编写。
EchoTcpClient.cc
在用户端,首先,我们也要进行socket的创建,创建好socket后,那么我们就要进行IP和port的绑定,但是有个问题,就是对于IP 和prot是否需要进行显示bind呢?
为了防止在服务器端,出现用户的端口号冲突的问题,所以不需要我们去手动bind,OS会对我们进行随机bind端口号。防止端口号冲突。
那么我们用户端创建好socket后,那么又该干嘛呢?
那么就要去和我们的服务端创建连接了。
那么在用户端要使用如下接口进行连接:

那么当我们进行连接的时候,其会为我们自动进行bind。

//建立连接和bind
InetAddr serveraddress(server_port,server_ip);
int n = connect(sockfd,(struct sockaddr*)serveraddress.GetNetAddress(),serveraddress.Len());
if(n<0)
{
std::cerr<<"connect to"<<serveraddress.ToString()<<"failed!";
exit(3);
}
std::cerr<<"connect to"<<serveraddress.ToString()<<"success";
//进行通信
while(true)
{
std::string line;
std::cout<<"please Enter#";
std::getline(std::cin,line);
write(sockfd,line.c_str(),line.size());
char inbuffer[1024]={0};
ssize_t n=read(sockfd,inbuffer,sizeof(inbuffer));
if(n>0)
{
inbuffer[n]=0;
std::cout<<inbuffer<<std::endl;
}
else if(n==0)
{
std::cout<<"read enf of flie"<<std::endl;
break;
}
else{
std::cerr<<"read error!"<<std::endl;
break;
}
那么还有一个问题,就是当我们进行连接,是一定就马上连接成功么?
实际上,我们平时进行打游戏也会有感觉,有时候会进行多次连接才会连接上,所以我们进行TCP连接的时候,是进行判断是否连接成功,然后进行循环连接的情况。然后如果连接多次的情况,那么我们就会进行终止进程的感觉。

EchoTcpServer.hpp改写
前面我们所写的服务端,其是单线程的,那么就是说,当我们有用户再使用服务器的时候,其他用户再去使用,那么此时就不能马上得到消息反馈,所以呢,我们下面要对其进行改写,改写成多进程的方式,对于创建连接后,对于消息的收发,我们创建一个子进程来完成。
但是我们的主进程也要对这个子进程来进行回收,那么如果我们进行wait等待的话,那么不也是要进行阻塞等待么,不还是单进程的效果,虽然我们可以使用waitpid来设置非阻塞等待,但是这个效果也不是很好。我们要的多进程的版本是,我们的主线程只去进行连接的工作,而我们的子进程就去完成通信工作,那么我们可以在我们的建立连接前,进行一个信号捕捉的工作,然后将子进程退出的时候返回的退出信号进行捕捉,进行一个忽略处理,那么子进程的退出我们主进程也进行处理了。而且也不需要进行wait等待子进程退出了。
那么我们还有一个方法,就是我们创建了子进程后,我们的子进程又立马去创建一个子进程,也就是创建孙子进程,然后由孙子进程去处理通信工作,我们的子进程立马退出,那么我们的主进程在创建好子进程后,立马就会等待到子进程的退出,那么就可以马上返回到开始进行等待连接的工作了。然后对于孙子进程其就变成孤儿进程,那么后续会被我们的系统进行回收处理。
然后还要补充的是,对于我们的主进程,我们再创建了子进程后,那么我们就不需要连接后返回的那个文件描述符了,所以为了资源的利用效率,我们要将其关闭,对于子进程也是如此,我们子进程不关心连接工作,只需要进行通信即可,所以我们要将对于的连接文件描述符关闭。
代码如下:
void Start()
{
while (true)
{
// signal(SIGCHLD, SIG_IGN); // 最佳实践
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
// 建立连接
int sockfd = accept(_lisentsockfd, (struct sockaddr *)&clientaddr, &len);
if (sockfd)
{
LOG(LogLevel:
:WARNING) << "accept errr!";
continue;
}
LOG(LogLevel::DEBUG) << "accept success,sockfd:" << sockfd;
// 处理链接
//单进程版本
// InetAddr clientaddress(clientaddr);
// serviceIO(sockfd, clientaddress);
//2、多进程版本
pid_t id = fork();
if(id < 0)
{
LOG(LogLevel::FATAL) << "fork errr!";
exit(FORK_ERR);
}
else if(id == 0)
{
// 关闭掉自己不需要的sockfd
close(_listensockfd);
// 子进程
if(fork() > 0) exit(0); // 子进程直接退出
// 孙子进程 - 孤儿进程
InetAddr clientaddress(clientaddr);
serviceIO(sockfd, clientaddress);
close(sockfd);
exit(0); // 重要
}
else{
// 父进程
// wait(id); waitpid: WNOHANG
// 关闭掉自己不需要的sockfd
close(sockfd);
pid_t rid = waitpid(id, nullptr, 0);
(void)rid;
}
close(sockfd);
}
}
上面我们完成的是从单进程到多进程的改写,那么创建进程的开销相对于创建线程还是比较大的,所以我们还可以将其改成多线程的方式。那么这样效率就高很多了。
但是还有个疑问就是,当我们创建新线程,那么我们的主线程不也要对新创建的线程进行回收么?
所以我们可以将新线程设置成分离状态,那么我们主线程也就不用进行join了。
然后对于线程的处理函数,我们知道其参数只能是一个返回值为void*参数也为void*的函数,那么我们在类中可以将其设置成static静态全局的,但是又有个问题了,就是在这个函数中,我们又无法获取到this指针了,那么我们可以选择在这个服务端类中在创建一个类,这个类中我们设置一个服务端指针,然后在线程任务的时候将这个指针进行传入,那么我们就可以得到this指针了,那么在该函数中就可以获取到服务端方法了。
代码如下:
class ThreadData
{
public:
ThreadData(TcpServer *ts, int sockfd, const InetAddr &addr)
: _this(ts), _sockfd(sockfd), _addr(addr)
{
}
~ThreadData()
{
close(_sockfd);
}
public:
TcpServer *_this;
int _sockfd;
InetAddr _addr;
};
static void *thread_routine(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
pthread_detach(pthread_self());
td->_this->serviceIO(td->_sockfd, td->_addr);
delete td;
return nullptr;
}
//ersion 2: 多线程版本
pthread_t tid;
InetAddr clientaddress(clientaddr);
ThreadData *td = new ThreadData(this, sockfd, clientaddress);
pthread_create(&tid, nullptr, thread_routine, (void *)td);
但是也要补充一点,就是未来我们服务端,其是一个长服务,那么如果我们如上一直创建多线程,那么我们的OS压力会很大,我们上面的方式,只合适在短服务上进行,对于长服务,我们后续会学习一个多路转发的方式。
所以不论是多线程还是多进程方式,我们上面的方式都只适合使用在多连接,短服务上,所以我们将IO服务进行一个修改,我们的线程,当执行了信息收发后,然后马上退出,当其要继续进行收发,然后其再重新进行申请。

对于这种短链接,短服务,可能我们创建线程的花销都要比处理任务的花销要大,所以我们可以引入我们前面写的线程池。

那么就这样就可以将我们的任务通过线程池进行调用完成。
而且当任务过多的时候,也会将其放入到任务队列中,在合适的时候再进行调用。