Linux:socket套接字介绍及实现简单的tcp通信


tcp编程:面向连接,可靠传输,面向字节流

tcp客户端与服务端流程

  • 客户端:创建套接字,描述地址信息,发起连接请求,连接建立成功,收发数据,关闭

  • 服务端:创建套接字,描述地址信息,开始监听,接受连接请求,新建套接字,获取新建套接字描述符,通过这个描述符与客户端通信,关闭

在这里插入图片描述

socket接口介绍

  1. 创建套接字
int socket(int domain, int type, int protocol)  - ( AF_INET, SOCK_STREAM - 流式套接字, IPPROTO_TCP);

  1. 绑定地址信息
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()
};
  1. 服务端开始监听
int listen(int sockfd, int backlog); - 告诉操作系统开始接收连接请求

参数

  • sockfd: 监听套接字 - 获取客户端连接请求的套接字
  • backlog: 决定同一时间,服务端所能接受的客户端连接请求数量

SYN泛洪攻击

  • 恶意主机不断的向服务端主机发送大量的连接请求,若服务端为每一个连接请求建立socket,则会瞬间资源耗尽。

服务器崩溃因此服务器端有一个connection pending queue;

  • 存放为连接请求新建的socket节点
  • backlog参数决定了这个队列的最大节点数量
  • 若这个队列放满了,若还有新连接请求到来,则将这个后续请求丢弃掉

在这里插入图片描述

  1. 获取新建socket的操作句柄

从内核指定socket的pending queue中取出一个socket,返回操作句柄

int accept(int sockfd, struct sockaddr *addr, socklen_t *len)

参数:

  • sockfd: 监听套接字 — 指定要获取哪个pending queue中的套接字
  • addr: 获取一个套接字,这个套接字与指定的客户端进行通信,通过addr获取这个客户端的地址信息
  • len: 输入输出型参数 — 指定地址信息想要的长度以及返回实际的地址长度

返回值: 成功则返回新获取的套接字的描述符; 失败返回-1

  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;若连接断开触发异常

  1. 关闭套接字:释放资源
int close(int fd);
  1. 客户端向服务端发送连接请求
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;
}

连接断开在发送端与接受端上的表现:

  1. 接受端:连接断开,则recv返回0(套接字写端被关闭 — 双工通信)

  2. 发送端:连接断开,则send触发异常 — SIGPIPE,导致进程退出

知识点习题:

  1. Linux中,一个端口能够接受tcp链接数量的理论上限是?

A. 1024
B. 65535
C. 65535 * 65535
D. 无上限

正确答案:D

答案解析

一个端口可以建立的连接数量没有理论上限,上限就是你系统的性能

  1. 下列关于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时间

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值