tcp通信
tcp编程:面向连接,可靠传输,面向字节流
tcp客户端与服务端流程
- 客户端:创建套接字,描述地址信息,发起连接请求,连接建立成功,收发数据,关闭
- 服务端:创建套接字,描述地址信息,开始监听,接受连接请求,新建套接字,获取新建套接字描述符,通过这个描述符与客户端通信,关闭
socket接口介绍
- 创建套接字
int socket(int domain, int type, int protocol) - ( AF_INET, SOCK_STREAM - 流式套接字, IPPROTO_TCP);
- 绑定地址信息
int bind(int sockfd, struct sockaddr *addr, socklen_t len);
struct sockaddr_in{
sin_family = AF_INET;
sin_port = htons();
sin_addr.s_ddr = int _addr()
};
- 服务端开始监听
int listen(int sockfd, int backlog); - 告诉操作系统开始接收连接请求
参数
- sockfd: 监听套接字 - 获取客户端连接请求的套接字
- backlog: 决定同一时间,服务端所能接受的客户端连接请求数量
SYN泛洪攻击:
- 恶意主机不断的向服务端主机发送大量的连接请求,若服务端为每一个连接请求建立socket,则会瞬间资源耗尽。
服务器崩溃因此服务器端有一个connection pending queue;
- 存放为连接请求新建的socket节点
- backlog参数决定了这个队列的最大节点数量
- 若这个队列放满了,若还有新连接请求到来,则将这个后续请求丢弃掉
- 获取新建socket的操作句柄
从内核指定socket的pending queue中取出一个socket,返回操作句柄
int accept(int sockfd, struct sockaddr *addr, socklen_t *len)
参数:
- sockfd: 监听套接字 — 指定要获取哪个pending queue中的套接字
- addr: 获取一个套接字,这个套接字与指定的客户端进行通信,通过addr获取这个客户端的地址信息
- len: 输入输出型参数 — 指定地址信息想要的长度以及返回实际的地址长度
返回值: 成功则返回新获取的套接字的描述符; 失败返回-1
- 通过新获取的套接字操作句柄(accept返回的描述符)与指定的客户端进行通信
接收数据:
ssize_t recv(int sockfd - accept返回的新建套接字描述符, char *buf, int len, int flag);
返回值: 成功返回实际读取的数据长度,连接断开返回0;读取失败返回-1
发送数据:
ssize_t send(int sockfd, char *data, int len, int flag);
返回值:成功返回实际发送的数据长度;失败返回-1;若连接断开触发异常
- 关闭套接字:释放资源
int close(int fd);
- 客户端向服务端发送连接请求
int connect(int sockfd, int sockaddr *addr, socklen_t len);
参数:
- sockfd: 客户端套接字 — 若还未绑定地址,则操作系统会选择合适的源端地址进行绑定
- addr: 服务端地址信息 — struct sockaddr_in; 这个地址信息经过connect之后也会描述道socket中
- len: 地址信息长度
实现tcp通信程序
tcpsocket.hpp
// 封装实现一个tcpsocket类,向外提供简单接口:
// 使外部通过实例化一个tcpsocket对象就能完成tcp通信程序的建立
#include <cstdio>
#include <string>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define BACKLOG 10
#define CHECK_RET(q) if((q)== false){return -1;}
class TcpSocket{
public:
TcpSocket():_sockfd(-1){
}
int GetFd(){
return _sockfd;
}
void SetFd(int fd){
_sockfd = fd;
}
// 创建套接字
bool Socket(){
// socket(地址域,套接字类型,协议类型)
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(_sockfd < 0){
perror("socket error");
return false;
}
return true;
}
void Addr(struct sockaddr_in *addr, const std::string &ip, uint16_t port){
addr->sin_family = AF_INET;
addr->sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(addr->sin_addr.s_addr));
}
// 绑定地址信息
bool Bind(const std:: string &ip, const uint16_t port){
// 定义IPv4地址结构
struct sockaddr_in addr;
Addr(&addr, ip, port);
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 = BACKLOG){
// listen(描述符,同一时间的并发链接数)
int ret = listen(_sockfd, backlog);
if(ret < 0){
perror("listen error");
return false;
}
return true;
}
// 客户端发起连接请求
bool Connect(const std::string &ip, const uint16_t port){
// 1.定义IPv4地址结构,赋予服务端地址信息
struct sockaddr_in addr;
Addr(&addr, ip, port);
// 2.向服务端发起请求
// 3.connect(客户端描述符,服务端地址信息,地址长度)
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, std::string *ip = NULL, uint16_t *port = NULL){
// accept(监听套接字,对端地址信息,地址信息长度)返回新的描述符
struct sockaddr_in addr;
socklen_t len = sizeof(struct sockaddr_in);
// 获取新的套接字,以及这个套接字对应的对端地址信息
int clisockfd = accept(_sockfd, (struct sockaddr*)&addr, &len);
if(clisockfd < 0){
perror("accept error");
return false;
}
// 用户传入了一个Tcpsocket对象的指针
// 为这个对象的描述符进行赋值 --- 赋值为新建套接字的描述符
// 后续与客户端的通信通过这个对象就可以完成
sock->_sockfd = clisockfd;
if(ip != NULL){
*ip = inet_ntoa(addr.sin_addr); // 网络字节序ip->字符串IP
}
if(port != NULL){
*port = ntohs(addr.sin_port);
}
return true;
}
// 发送数据
bool Send(const std::string &data){
// send(描述符,数据,数据长度,选项参数)
int ret = send(_sockfd, data.c_str(), data.size(), 0);
if(ret < 0){
perror("send error");
return false;
}
return true;
}
// 接收数据
bool Recv(std::string *buf){
// recv(描述符,缓冲区,数据长度,选项参数)
char tmp[4096] = {0};
int ret = recv(_sockfd, tmp, 4096, 0);
if(ret < 0){
perror("recv error");
return false;
}
else if(ret == 0){
printf("connection break\n");
return false;
}
buf->assign(tmp, ret); // 从tmp中拷贝ret大小的数据到buf中
return true;
}
// 关闭套接字
bool Close(){
close(_sockfd);
_sockfd = -1;
return true;
}
private:
int _sockfd;
};
tcp_srv.cpp
// 使用封装的TcpSocket类实例化对象实现tcp服务端程序
#include <iostream>
#include "tcpsocket.hpp"
int main(int argc, char *argv[]){
if(argc != 3){
printf("em:./tcp_srv 192.168.122.132 9000\n");
return -1;
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]); // stoi将字符串转换为数字
TcpSocket lst_sock;
CHECK_RET(lst_sock.Socket());
CHECK_RET(lst_sock.Bind(ip, port));
CHECK_RET(lst_sock.Listen());
while(1){
TcpSocket cli_sock;
std::string cli_ip;
uint16_t cli_port;
// Accept类成员函数,使用的私有成员_sockfd就是lst_sock的私有成员
// cli_sock取地址传入,目的是为了获取accept接口返回的通信套接字描述符
bool ret = lst_sock.Accept(&cli_sock, &cli_ip, &cli_port);
if(ret == false){
// 获取新连接失败,可以重新继续获取下一个
continue;
}
printf("new connect: [%s:%d]\n", cli_ip.c_str(), cli_port);
// 通过新获取的通信套接字与客户端进行通信
std::string buf;
if(cli_sock.Recv(&buf) == false){
cli_sock.Close(); // 通信套接字接收数据出错,关闭的是通信套接字
continue;
}
printf("client:[%s:%d] say:%s\n", &cli_ip[0], cli_port, &buf[0]);
std::cout << "server say:";
fflush(stdout);
buf.clear();
std::cin >> buf;
if(cli_sock.Send(buf) == false){
cli_sock.Close();
continue;
}
}
lst_sock.Close();
return 0;
}
tcp_cli.cpp
// 通过封装的TcpSocket类实例化对象实现tcp客户端程序
#include <iostream>
#include "tcpsocket.hpp"
int main(int argc, char *argv[]){
if(argc != 3){
printf("em:./tcp_cli 192.168.122.132 9000 - 服务绑定的地址\n");
return -1;
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
TcpSocket cli_sock;
// 创建套接字
CHECK_RET(cli_sock.Socket());
// 绑定地址信息(不推荐)
// 向服务端发起请求
CHECK_RET(cli_sock.Connect(ip, port));
// 循环收发数据
while(1){
printf("client say:");
fflush(stdout);
std::string buf;
std::cin >> buf;
// 因为客户端不存在多种套接字的文件,因此一旦当前套接字出错直接退出就行
// 进程退出就会释放资源,关闭套接字
CHECK_RET(cli_sock.Send(buf));
buf.clear();
CHECK_RET(cli_sock.Recv(&buf));
printf("server say:%s\n", buf.c_str());
}
cli_sock.Close();
return 0;
}
代码生成
tcp服务端程序无法持续与客户端进行通信:
具体技术:
多线程/多进程
多进程
- 父进程创建子进程,数据独有,各自有一份cli_sock;然而子进程通过cli_sock通信,但是父进程不需要,因此父进程关闭自己的cli_sock
- 父进程要等待子进程退出,避免产生僵尸进程;为了父进程只负责获取新连接,因此对于SIGCHLD信号自定义处理回调等待
服务端代码
// 使用封装的TcpSocket类实例化对象实现tcp服务端程序
#include <iostream>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#include "tcpsocket.hpp"
void sigcb(int signo){
// 当子进程退出的时候就会向父进程发送SIGCHLD信号,回掉这个函数
// waitpid返回值>0表示处理了一个退出的子进程
// waitpid<=0 表示没有退出的子进程
while(waitpid(-1, 0, WNOHANG) > 0); // 一次回调循环将退出的子进程全部处理
}
int main(int argc, char *argv[]){
if(argc != 3){
printf("em:./tcp_srv 192.168.122.132 9000\n");
return -1;
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]); // stoi将字符串转换为数字
signal(SIGCHLD, sigcb);
TcpSocket lst_sock;
CHECK_RET(lst_sock.Socket());
CHECK_RET(lst_sock.Bind(ip, port));
CHECK_RET(lst_sock.Listen());
while(1){
TcpSocket cli_sock;
std::string cli_ip;
uint16_t cli_port;
bool ret = lst_sock.Accept(&cli_sock, &cli_ip, &cli_port);
if(ret == false){
continue;
}
printf("new connect: [%s:%d]\n", cli_ip.c_str(), cli_port);
// ------------------------------------------------------
pid_t pid = fork();
if(pid == 0){ // 子进程复制父进程 - 数据独有,代码共享
// 让子进程处理与客户端通信
while(1){
// 通过新获取的通信套接字与客户端进行通信
std::string buf;
if(cli_sock.Recv(&buf) == false){
cli_sock.Close(); // 通信套接字接收数据出错,关闭的是通信套接字
exit(0);
}
printf("client:[%s:%d] say:%s\n", &cli_ip[0], cli_port, &buf[0]);
std::cout << "server say:";
fflush(stdout);
buf.clear();
std::cin >> buf;
if(cli_sock.Send(buf) == false){
cli_sock.Close();
exit(0);
}
}
cli_sock.Close();
exit(0);
}
// 父子进程数据独有,都会具有cli_sock,但是父进程并不通信
cli_sock.Close(); // 这个关闭对子进程没有影响,数据各自有一份
}
lst_sock.Close();
return 0;
}
多线程
- 主线程获取到新连接然后创建新线程与客户端进行通信,但是需要将套接字描述符传入线程执行函数中
- 但是传输这个描述符的时候,不能使用局部变量的地址传递(局部变量的空间在循环完毕就会被释放),可以传描述符的值,也可以传入new的对象
- c++ 中对于类型强转,将数据值当作指针传递有很多限制,我们想办法去克服就可以了
- 主线程中虽然不使用cli_sock,但是不能关闭cli_sock,因为线程间共享资源,一个线程释放,另一个线程也就没法使用了
// 使用封装的TcpSocket类实例化对象实现tcp服务端程序
#include <iostream>
#include <stdlib.h>
#include "tcpsocket.hpp"
void *thr_start(void *arg){
long fd = (long)arg;
TcpSocket cli_sock;
cli_sock.SetFd(fd);
while(1){
// 通过新获取的通信套接字与客户端进行通信
std::string buf;
if(cli_sock.Recv(&buf) == false){
cli_sock.Close(); // 通信套接字接收数据出错,关闭的是通信套接字
pthread_exit(NULL); // exit是退出进程
}
printf("client say:%s\n", &buf[0]);
std::cout << "server say:";
fflush(stdout);
buf.clear();
std::cin >> buf;
if(cli_sock.Send(buf) == false){
cli_sock.Close();
pthread_exit(NULL);
}
}
// 循环退出则关闭套接字
cli_sock.Close();
return NULL;
}
int main(int argc, char *argv[]){
if(argc != 3){
printf("em:./tcp_srv 192.168.122.132 9000\n");
return -1;
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]); // stoi将字符串转换为数字
TcpSocket lst_sock;
CHECK_RET(lst_sock.Socket());
CHECK_RET(lst_sock.Bind(ip, port));
CHECK_RET(lst_sock.Listen());
while(1){
TcpSocket cli_sock;
std::string cli_ip;
uint16_t cli_port;
// Accept类成员函数,使用的私有成员_sockfd就是lst_sock的私有成员
// cli_sock取地址传入,目的是为了获取accept接口返回的通信套接字描述符
bool ret = lst_sock.Accept(&cli_sock, &cli_ip, &cli_port);
if(ret == false){
// 获取新连接失败,可以重新继续获取下一个
continue;
}
printf("new connect: [%s:%d]\n", cli_ip.c_str(), cli_port);
// --------------------------------------
pthread_t tid;
// 将通信套接字当作参数传递给线程,让线程与客户端进行通信
// cli_sock是一个局部变量 - 循环完了这个资源就会被释放
pthread_create(&tid, NULL, thr_start, (void*)cli_sock.GetFd()); // 线程
pthread_detach(tid); // 不关心线程返回值,分离线程,退出后自动释放资源
// 主线程不能关闭cli_sock套接字,因为多线程是公用描述符的
}
lst_sock.Close();
return 0;
}
连接断开在发送端与接受端上的表现:
- 接受端:连接断开,则recv返回0(套接字写端被关闭 — 双工通信)
- 发送端:连接断开,则send触发异常 — SIGPIPE,导致进程退出
知识点习题:
- Linux中,一个端口能够接受tcp链接数量的理论上限是?
A. 1024
B. 65535
C. 65535 * 65535
D. 无上限
正确答案:D
答案解析:
一个端口可以建立的连接数量没有理论上限,上限就是你系统的性能
- 下列关于TCP的握手和挥手的描述,正确的是:
A. 短连接更容易引起半连接队列移除
B. FIN和ACK不可能在同一个包里
C. 进入TIME_WAIT状态需要等待MSL时间
D. SYN和ACK可能在同一个包里
正确答案: A D
答案解析:
全连接队列、半连接队列溢出这种问题很容易被忽视,但是又很关键,特别是对于一些短连接应用(比如Nginx、PHP,当然他们也是支持长连接的)更容易爆发。 一旦溢出,从cpu、线程状态看起来都比较正常,但是压力上不去,在client看来rt也比较高(rt=网络+排队+真正服务时间),但是从server日志记录的真正服务时间来看rt又很短。
TCP释放连接时time_wait状态必须等待2MSL时间