上次我们模拟实现了简易的UDP通信, 不知道大家有没有实现啊~
今天我们就来看看另一个非常非常非常重要的协议 — TCP协议
文章目录
TCP通信流程
TCP — 传输控制协议
面向连接, 可靠传输, 面向字节流
tcp协议用于安全性要求大于实时性的场景 — 文件传输
下面给出TCP通信中客户端和服务端的各自的流程
socket接口
头文件<sys/socket.h>
1. 创建套接字
int socket(int domain, int type, int protocol);
domain: 地址域类型 -- AF_INET (ipv4地址域)
type: 套接字类型 -- SOCK_STREAM 字节流服务 / SOCK_DGRAM数据报服务
protocol: 协议类型, 0 默认类型/ IPPROTO_TCP/ IPPROTO_UDP
字节流服务默认TCP协议, 数据报服务默认是UDP服务
返回值: 成功返回一个非负整数 -- 套接字描述符, 失败返回-1
2. 为套接字绑定地址信息
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd: 创建套接字返回的操作句柄
addr: struct socketaddr结构体, 不同的地址域有对应不同的地址结构
addrlen: 实际地址结构的长度
返回值: 成功返回0, 失败返回-1
3. 客户端发起连接请求
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
addr: 服务端的地址信息
addrlen: 地址信息长度
4. 服务端开始监听
int listen(int sockfd, int backlog);
backlog: 服务端最大并发连接数
(服务端同一时间能接收的客户端请求连接数, 并非总连接数)
5. 获取新建的客户端连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd: 套接字描述符 -- 监听套接字
addr: 新建socket连接对应的客户端地址
返回值: 成功返回新建客户端套接字的描述符--操作句柄, 失败返回-1
注意:
最早创建的并且listen的套接字, 只负责获取新建连接, 不与客户端通信
accept返回的套接字描述符(新建的套接字), 才是真正与客户端通信的套接字
6. 收发数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
返回值: 错误返回-1, 连接断开返回0, 成功返回实际接收的数据长度
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
这里的sockfd, 对于服务端来说一定是accept返回的描述符
7. 关闭套接字
int close(sockfd);
8. 地址转换接口
htons/htonl: 主机字节序到网络字节序的整数转换 short-16/int-32
ntohs/ntohl: 网络字节序到主机字节序的整数转换 short-16/int-32
点分十进制字符串IP地址到网络字节序IP地址的转换
in_addr_t inet_addr(const char *cp);
网络字节序IP地址到点分十进制字符串IP地址的转换
char *inet_ntoa(struct in_addr in);
int inet_pton(int af, const char *src, void *dst);
af: 地址域类型 -- AF_INET, src: 字符串IP地址,
dst: 返回转换后的整数地址
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af: 地址域类型, src: 网络字节序整数IP,
dst: 返回转换后的字符串, size: dst空间长度
代码实现TCP通信
1. 封装socket接口
创建tcpsocket.hpp头文件, 进行接口的封装
#include <iostream>
#include <string>
#include <unistd.h>
#include <arpa/inet.h> //地址转换接口头文件
#include <netinet/in.h> // 地址结构类型定义头文件
#include <sys/socket.h> //套接字接口头文件
using namespace std;
#define CHECK_RET(q) if((q)==false){return -1;}
class TcpSocket {
public:
TcpSocket() :_sockfd(-1) {}
bool Socket() {
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(_sockfd < 0) {
perror("socket error");
return false;
}
return true;
}
//绑定地址信息
bool Bind(const string& ip, const int 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(struct sockaddr_in);
int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
if(ret < 0) {
perror("bind error");
return false;
}
return true;
}
//服务端开始监听
bool Listen(int backlog = 5) {
//int listen(int sockfd, backlog)
int ret = listen(_sockfd, backlog);
if (ret < 0) {
perror("listen error");
return false;
}
return true;
}
//客户端请求新连接
bool Connect(const string& ip, int port) {
//int connect(int sockfd, sockaddr* srvaddr, int addrlen)
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(struct sockaddr_in);
int ret = connect(_sockfd, (struct sockaddr*)&addr, len);
if (ret < 0) {
perror("connect error");
return false;
}
return true;
}
//服务端获取新连接
bool Accept(TcpSocket* sock, string* ip = nullptr, int* port = nullptr) {
//int accept(int sockfd, sockaddr* addr, int* len);
struct sockaddr_in cli_addr;
socklen_t len = sizeof(struct sockaddr_in);
int fd = accept(_sockfd, (struct sockaddr*)&cli_addr, &len);
if(fd < 0) {
perror("accept error");
return false;
}
//传入的sock对象, 获取新建的连接套接字
//fd是与cli_addr地址的客户端进行通信的, 外部通过sock与客户端进行通信
sock->_sockfd = fd;
if (ip != nullptr) {
*ip = inet_ntoa(cli_addr.sin_addr);
}
if(port != nullptr) {
*port = ntohs(cli_addr.sin_port);
}
return true;
}
//接收数据
bool Recv(string* buf) {
//ssize_t recv(int sockfd, char* buf, int len, len)
//返回值: 错误返回-1, 连接断开返回0, 成功返回实际接收的数据长度
char tmp[4096] = {0};
int ret = recv(_sockfd, tmp, 4096, 0);
if (ret < 0) {
perror("recv error");
return false;
}
else if (ret == 0) {
printf("connect shutdown");
return false;
}
buf->assign(tmp, ret);
return true;
}
//发送数据
bool Send(const string& data) {
//ssize_t send(int sockfd, char* data, int len, int flag)
size_t total_len = 0; //实际发送的数据长度
while(total_len < data.size()) {
int ret = send(_sockfd, data.c_str() + total_len,
data.size() - total_len, 0);
if (ret < 0) {
perror("send error");
return false;
}
total_len += ret;
}
return true;
}
//关闭套接字
bool Close () {
if(_sockfd < 0) {
close(_sockfd);
_sockfd = -1;
}
return true;
}
private:
int _sockfd;
};
2. 模拟实现服务端
创建tcp_server.cpp文件, 进行服务端的实现
#include "tcpsocket.hpp"
int main() {
//监听套接字
TcpSocket lst_sock;
//1.创建套接字
CHECK_RET(lst_sock.Socket());
//2.绑定地址信息
CHECK_RET(lst_sock.Bind("0.0.0.0", 9000));
//3.开始监听
CHECK_RET(lst_sock.Listen());
while(1) {
//4.获取新连接
TcpSocket con_sock;
string cli_ip;
int cli_port;
int ret = lst_sock.Accept(&con_sock, &cli_ip, &cli_port);
if (ret == false) {
//获取失败就重新去获取
continue;
}
//5.收发数据
string buf;
ret = con_sock.Recv(&buf);
if (ret == false) {
con_sock.Close();
continue;
}
printf("[%s:%d] say:%s\n", cli_ip.c_str(), cli_port, buf.c_str());
string data;
cout << "server say: ";
cin >> data;
ret = con_sock.Send(data);
if (ret == false) {
con_sock.Close();
continue;
}
}
//6.关闭套接字
lst_sock.Close();
return 0;
}
3. 模拟实现客户端
创建tcp_client.cpp文件, 进行服务端的实现
#include "tcpsocket.hpp"
using namespace std;
int main() {
TcpSocket cli_sock;
//1.创建套接字
CHECK_RET(cli_sock.Socket());
//2.绑定地址信息(不推荐)
//3.向服务端发起连接请求
CHECK_RET(cli_sock.Connect("172.17.0.8", 9000));
while(1) {
//4.发送数据
string data;
cout << "client say: ";
cin >> data;
CHECK_RET(cli_sock.Send(data));
//5.接收数据
string buf;
CHECK_RET(cli_sock.Recv(&buf));
cout << "server say: " << buf << endl;
}
//6.关闭套接字
cli_sock.Close();
return 0;
}
到这里就实现完了~
编译后运行即可, 但是大家会发现tcp只能通信一次
原因: 服务端监听的时候, 获取新连接后收发一次数据, 就会再次进入下一次循环, 阻塞等待获取下一个新连接, 没有新连接就不会进行数据收发
那怎么解决呢?
把获取新连接放外面可以吗?
当然不行 ! ! 因为放外面的话, 只会获取一次新连接, 只能和一个用户进行连续通信
想要实现和多用户进行连续通信也是可以的, 这篇长度太长了, 就写到下一篇吧~
附上链接: 实现多执行流并发TCP服务器