TCP的通信流程
计算机网络 (三) 传输层 :一文搞懂UDP与TCP协议
在这篇博客中,我描述了UDP与TCP的特性以及通信流程,下面就根据特性来规划该如何通过Socket来实现UDP通信。
TCPSocket的封装
为了使用更方便,先封装一个TCPSocket
#include<iostream>
#include<string>
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
const int MAX_LISTEN = 5;
inline void CheckSafe(bool ret)
{
if(ret == false)
{
exit(0);
}
}
class TcpSocket
{
public:
TcpSocket() : _socket_fd(-1)
{}
//创建套接字
bool Socket()
{
_socket_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(_socket_fd < 0)
{
std::cerr << "socket create error" << std::endl;
return false;
}
return true;
}
//绑定地址信息
bool Bind(const std::string& ip, uint16_t& port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(sockaddr_in);
int ret = bind(_socket_fd, (sockaddr*)&addr, len);
if(ret < 0)
{
std::cerr << "bind error" << std::endl;
return false;
}
return true;
}
//监听
bool Listen(int backlog = MAX_LISTEN)
{
//用初始的套接字开始监听
int ret = listen(_socket_fd, backlog);
if(ret < 0)
{
std::cerr << "connect error" << std::endl;
}
return true;
}
//新建连接
bool Accept(TcpSocket *new_sock, std::string* ip = NULL, uint16_t* port = NULL)
{
struct sockaddr_in addr;
socklen_t len = sizeof(sockaddr_in);
//创建一个新的套接字与客户端建立连接
int new_fd = accept(_socket_fd, (sockaddr*)&addr, &len);
if(new_fd < 0)
{
std::cerr << "accept error" << std::endl;
}
new_sock->_socket_fd = new_fd;
if(ip != NULL)
{
*ip = inet_ntoa(addr.sin_addr);
}
if(port != NULL)
{
*port = ntohs(addr.sin_port);
}
return true;
}
//发起连接请求
bool Connect(const std::string& ip, uint16_t port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(sockaddr_in);
int ret = connect(_socket_fd, (sockaddr*)&addr, len);
if(ret < 0)
{
std::cerr << "connect error" << std::endl;
}
return true;
}
//发送数据
bool Send(const std::string& data)
{
int ret = send(_socket_fd, data.c_str(), data.size(), 0);
if(ret < 0)
{
std::cerr << "send error" << std::endl;
}
return true;
}
//接收数据
bool Recv(std::string& data)
{
char buff[4096] = { 0 };
int ret = recv(_socket_fd, buff, 4096, 0);
if(ret == 0)
{
std::cerr << "connect error" << std::endl;
return false;
}
else if(ret < 0)
{
std::cerr << "recv error" << std::endl;
return false;
}
data.assign(buff, ret);
return true;
}
void Close()
{
if(_socket_fd > 0)
{
close(_socket_fd);
_socket_fd = -1;
}
}
private:
int _socket_fd;
};
TCP客户端
按照这个流程,来实现TCP的客户端
#include<iostream>
#include"TcpSocket.hpp"
using namespace std;
int main(int argc, char* argv[])
{
if(argc != 3)
{
cerr << "正确输入方式: ./tcp_cli.cc ip port\n" << endl;
return -1;
}
string srv_ip = argv[1];
uint16_t srv_port = stoi(argv[2]);
TcpSocket socket;
//创建套接字
CheckSafe(socket.Socket());
//申请连接服务器
CheckSafe(socket.Connect(srv_ip, srv_port));
while(1)
{
string data;
cout << "cli send message: ";
getline(cin, data);
if(data == "quit")
{
break;
}
//发送数据
CheckSafe(socket.Send(data));
data.clear();
//接收数据
CheckSafe(socket.Recv(data));
cout << "srv recv message :" << data << endl;
}
//关闭套接字
socket.Close();
return 0;
}
TCP服务器
因为在一段时间内可能会建立多个连接,所以多个执行流分别去控制这些连接。每当创建一个新连接,就分配一个新的执行流去执行它。
下面就分别实现多进程版本和多线程版本的服务器
多进程版本
注意:因为父子进程数据独有,子进程会拷贝一份父进程,所以对于父进程来说新建的套接字用不到,需要关闭。父进程也不需要去阻塞等待子进程,处理好信号即可
#include<iostream>
#include<signal.h>
#include<sys/wait.h>
#include"TcpSocket.hpp"
using namespace std;
void sigcb(int no)
{
while(waitpid(-1, NULL, WNOHANG) > 0);
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
cerr << "正确输入方式: ./tcp_srv_process.cc. ip port\n" << endl;
return -1;
}
signal(SIGCHLD, sigcb);
string srv_ip = argv[1];
uint16_t srv_port = stoi(argv[2]);
TcpSocket socket;
//创建套接字
CheckSafe(socket.Socket());
//绑定地址信息
CheckSafe(socket.Bind(srv_ip, srv_port));
//开始监听
CheckSafe(socket.Listen());
while(1)
{
TcpSocket new_sock;
//通过监听套接字建立连接
CheckSafe(socket.Accept(&new_sock));
//创建子进程
int pid = fork();
if(pid == 0)
{
while(1)
{
string data;
//接收数据
CheckSafe(new_sock.Recv(data));
cout << "cli send message: " << data << endl;
data.clear();
cout << "srv reply message: ";
//发送数据
getline(cin, data);
CheckSafe(new_sock.Send(data));
}
//关闭子进程连接的套接字
new_sock.Close();
exit(0);
}
//关闭父进程套接字
new_sock.Close();
}
//关闭监听套接字
socket.Close();
return 0;
}
多线程版本
注意:因为线程之间共享描述符表,所以主线程创建线程之后千万不能关闭新建的套接字。
#include<iostream>
#include"TcpSocket.hpp"
#include<pthread.h>
using namespace std;
void* thr_work(void* arg)
{
//因为参数只能是void*,所以要强转获取操作句柄,所以读取前四个字节即可
long fd = (long)arg;
TcpSocket new_sock;
new_sock.SetFd(fd);
while(1)
{
string data;
//接收数据
new_sock.Recv(data);
cout << "cli send message: " << data << endl;
data.clear();
cout << "srv reply message: ";
//发送数据
getline(cin, data);
new_sock.Send(data);
}
new_sock.Close();
return nullptr;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
cerr << "正确输入方式: ./tcp_srv_thread.cc. ip port\n" << endl;
return -1;
}
string srv_ip = argv[1];
uint16_t srv_port = stoi(argv[2]);
TcpSocket lst_socket;
//创建监听套接字
CheckSafe(lst_socket.Socket());
//绑定地址信息
CheckSafe(lst_socket.Bind(srv_ip, srv_port));
//开始监听
CheckSafe(lst_socket.Listen());
while(1)
{
TcpSocket* new_sock = new TcpSocket();
//通过监听套接字获取连接
bool ret = lst_socket.Accept(new_sock);
//连接失败则重新连接
if(!ret)
{
cerr << "连接失败" << endl;
continue;
}
//创建线程
pthread_t tid;
int res = pthread_create(&tid, NULL, thr_work, (void*)new_sock->GetFd());
if(res != 0)
{
cerr << "线程创建失败" << endl;
continue;
}
//不需要知道返回值,所以直接分离线程
pthread_detach(tid);
}
//关闭监听套接字
lst_socket.Close();
return 0;
}