『网络』基于TCP协议的socket编程

概述

首先,我们知道,TCP是有连接的可靠的字节流协议。在基于TCP协议进行通信时,通信双方需要先建立一个TCP连接,建立连接需要进行三次握手,握手成功后才可以进行通信。
下图为典型的TCP客户端/服务器程序的函数调用
在这里插入图片描述

网络通信流程

客户端流程

  1. 创建套接字
  2. 不推荐手动绑定地址
  3. 服务器开始监听之后向服务器发起连接请求(向服务端发送SYN请求,服务端收到之后,恢复一个ACK同时向客户端发送一个SYN,客户端收到后,向服务器发送一个ACK)(三次握手)。
  4. 发送数据
  5. 接收数据
  6. 关闭套接字

服务端流程

  1. 创建套接字
  2. 绑定地址信息
  3. 开始监听告诉操作系统,可以接受客户端的连接请求了)。这个过程中,若有新的客户端连接请求到来,系统会为这个客户端新建一个socket提供一对一服务第一次握手就会新建一个socket然后将这个socket放到未完成连接队列。完成三次握手后,再将socket放到已完成连接队列中
  4. 获取已经连接成功的客户端socket,获取之后就可以通过这个新建的socket与指定客户端进行通信

接口介绍

创建套接字:
int socket(int domain, int type, int protocol);
参数:
	domin:地址域。
		AF_INET:IPV4网络协议地址域。
		AF_INET6:IPV6网络协议地址域。
	type:套接字类型。
		SOCK_STREAM:流式套接字,默认协议TCP,不支持UDP。
		SOCK_DGRAM:数据报套接字,默认协议UDP,不支持TCP。
	protocol:协议类型。
		0:试用套接字默认协议。
		6/IPPROTO_TCP:TCP协议。
		17/IPPROTO_UDP:UDP协议。
返回值:套接字操作句柄(文件描述符),失败返回-1
为套接字绑定地址信息:
int bind(int sockfd, const struct sockaddr *addr,
                socklen_t addrlen);
参数:
	sockfd:创建套接字返回的描述符。
	addr:地址信息。
	addrlen:地址信息长度。
返回值:成功返回0,失败返回-1
开始监听:
int listen(int sockfd, int backlog);
参数:
	sockfd:套接字描述符。
	backlog:已完成连接队列的最大节点数,决定了同一时间的最大并发连接数。
返回值:成功返回0,出错返回-1
请求连接:
int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);
参数:
	sockfd:套接字描述符。
	addr:发送端地址信息。
	addrlen:地址信息长度。
返回值:成功返回0,出错返回-1
获取已经连接成功的客户端:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
	sockfd:套接字描述符。
	addr:发送端地址信息。
	addrlen:地址信息长度。
返回值:返回新建的套接字的描述符,出错返回-1。
注意:该函数为阻塞获取已经完成的连接。
接收数据:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:
	sockfd:操作句柄,套接字描述符。
	buf:用buf存储接收的数据。
	len:想要接收的数据长度。
	flags:
		0:默认阻塞接受。
返回值:大于0为实际接收的数据长度,小于0失败,等于0连接断开。
发送数据:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数:
	socket:套接字描述符。
	buf:要发送的数据。
	len:要发送的数据长度。
	flags:
		0:默认阻塞发送。
返回值:实际发送的数据长度,失败返回-1
关闭套接字:
int close(int fd);
参数:
	fd:套接字描述符。
返回值:成功返回0,失败返回-1

封装TcpSocket类

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <errno.h>
#include <string>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
using std::cout;
using std::endl;

class TcpSocket{
	public:
		TcpSocket()
			: _sockfd(-1)
		{}

		void SetSockFd(int fd){
			_sockfd = fd;
		}

		bool Socket(){
			_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
			if(_sockfd < 0){
				perror("socket error");
				return false;
			}

			return true;
		}

		bool Bind(std::string& ip, uint16_t port){
			struct sockaddr_in addr;

			addr.sin_family = AF_INET;
			addr.sin_port = htons(port);
			inet_pton(AF_INET, ip.c_str(), &addr.sin_addr);

			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 = 10){
			int ret = listen(_sockfd, backlog);
			if(ret < 0){
				perror("listen error");
				return false;
			}

			return true;
		}

		bool Connect(std::string& ip, uint16_t port){
			struct sockaddr_in addr;

			addr.sin_family = AF_INET;
			addr.sin_port = htons(port);
			inet_pton(AF_INET, ip.c_str(), &addr.sin_addr);

			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& csock, struct sockaddr_in* addr = NULL){
			struct sockaddr_in _addr;
			socklen_t len = sizeof(struct sockaddr_in);

			int new_sockfd = accept(_sockfd, (struct sockaddr*)&_addr, &len);
			if(new_sockfd < 0){
				perror("accept error");
				return false;
			}

			if(addr != NULL){
				memcpy(addr, &_addr, len);
			}

			csock.SetSockFd(new_sockfd);

			return true;
		}

		bool Recv(std::string& buf){
			char temp[1024] = {0};

			int ret = recv(_sockfd, temp, 1024, 0);
			if(ret < 0){
				perror("recv error");
				return false;
			}
			else if(ret == 0){
				cout << "connect break!" << endl;
				return false;
			}

			buf.assign(temp, ret);

			return true;
		}

		bool Send(std::string& buf){
			int ret = send(_sockfd, buf.c_str(), buf.size(), 0);
			if(ret < 0){
				perror("send error");
				return false;
			}

			return true;
		}

		bool Close(){
			int ret = close(_sockfd);
			if(ret < 0){
				perror("close error");
				return false;
			}

			_sockfd = -1;

			return true;
		}

	private:
		int _sockfd;
};

TCP客户端和服务器的实现

TCP服务器

#include "tcp_socket.h"

int main(int argc, char* argv[]){
	if(argc != 3){
		cout << "./tcp_server ip port" << endl;
		return -1;
	}

	bool ret;
	std::string ip = argv[1];
	uint16_t port = atoi(argv[2]);

	TcpSocket sock;
	ret = sock.Socket();
	if(!ret){
		return -1;
	}

	ret = sock.Bind(ip, port);
	if(!ret){
		return -1;
	}

	ret = sock.Listen();
	if(!ret){
		return -1;
	}

	while(1){
		TcpSocket cli_sock;
		struct sockaddr_in cli_addr;

		if(!sock.Accept(cli_sock, &cli_addr)){
			continue;
		}

		cout << "new connect client: " << inet_ntoa(cli_addr.sin_addr) 
			<< ", "  << ntohs(cli_addr.sin_port) << endl;

		std::string buf;
		cli_sock.Recv(buf);
		cout << "client say: " << buf.c_str() << endl;

		buf.clear();

		cout << "server say: ";
		fflush(stdout);

		std::cin >> buf;
		cli_sock.Send(buf);
	}

	ret = sock.Close();
	if(!ret){
		return -1;
	}

	return 0;
}

TCP客户端

#include "tcp_socket.h"

int main(int argc, char* argv[]){
	if(argc != 3){
		cout << "./tcp_client ip port" << endl;
		return -1;
	}

	bool ret;
	std::string ip = argv[1];
	uint16_t port = atoi(argv[2]);

	TcpSocket sock;
	ret = sock.Socket();
	if(!ret){
		return -1;
	}

	ret = sock.Connect(ip, port);
	if(!ret){
		return -1;
	}

	while(1){
		std::string buf;

		cout << "client say: ";
		fflush(stdout);

		std::cin >> buf;
		sock.Send(buf);

		buf.clear();
		sock.Recv(buf);
		cout << "server say: " << buf << endl;
	}

	ret = sock.Close();
	if(!ret){
		return -1;
	}

	return 0;
}

上面实现的客户端服务端的通信是有问题的,因为accept是阻塞获取已经完成的连接(recv也是阻塞接收),所以每个客户端只能和服务器完成一次会话,然后服务器就会阻塞等待新的客户端连接。从而整个会话被阻塞
总结:因为accept是阻塞获取已经完成的连接而且recv也是阻塞获取数据,我么又不知道客户端连接请求什么时候来,客户端的数据什么时候发送,所以很容易造成程序的阻塞。可以使用多进程或者多线程解决这个问题。

TCP服务器(多进程)

#include <signal.h>
#include <sys/wait.h>
#include "tcp_socket.h"

struct sigaction act;

void myHandler(int signo){
	while(waitpid(-1, NULL, WNOHANG) > 0);
}

int main(int argc, char* argv[]){
	if(argc != 3){
		cout << "./tcp_server_multiprocess ip port" << endl;
		return -1;
	}

	act.sa_handler = myHandler;
	act.sa_flags = 0;
	sigemptyset(&act.sa_mask);
	sigaction(SIGCHLD, &act, NULL);

	bool ret;
	std::string ip = argv[1];
	uint16_t port = atoi(argv[2]);

	TcpSocket sock;
	ret = sock.Socket();
	if(!ret){
		return -1;
	}

	ret = sock.Bind(ip, port);
	if(!ret){
		return -1;
	}

	ret = sock.Listen();
	if(!ret){
		return -1;
	}

	while(1){
		TcpSocket cli_sock;
		struct sockaddr_in cli_addr;

		if(sock.Accept(cli_sock, &cli_addr) == false){
			continue;
		}

		cout << "new connect client: " << inet_ntoa(cli_addr.sin_addr)
			<< ", " << ntohs(cli_addr.sin_port) << endl;

		pid_t pid = fork();
		if(pid == 0){
			while(1){
				std::string buf;
				cli_sock.Recv(buf);
				cout << "client say: " << buf << endl;

				buf.clear();
				cout << "server say: ";
				fflush(stdout);
				std::cin >> buf;
				cli_sock.Send(buf);
			}
		}
		cli_sock.Close();
	}
	sock.Close();

	return 0;
}

TCP服务器(多线程)

#include "tcp_socket.h"
#include <pthread.h>

void* thr_start(void* arg){
	TcpSocket* sock = (TcpSocket*)arg;

	while(1){
		std::string buf;
		sock->Recv(buf);
		cout << "client say: " << buf << endl;

		buf.clear();
		cout << "server say: ";
		fflush(stdout);
		std::cin >> buf;
		sock->Send(buf);
	}

	sock->Close();
	delete sock;

	return NULL;
}

int main(int argc, char* argv[]){
	if(argc != 3){
		cout << "./tcp_server_multithreading ip port" << endl;
		return -1;
	}

	bool ret;
	std::string ip = argv[1];
	uint16_t port = atoi(argv[2]);

	TcpSocket sock;
	ret = sock.Socket();
	if(!ret){
		return -1;
	}

	ret = sock.Bind(ip, port);
	if(!ret){
		return -1;
	}

	ret = sock.Listen();
	if(!ret){
		return -1;
	}

	while(1){
		TcpSocket* cli_sock = new TcpSocket;
		struct sockaddr_in cli_addr;

		if(!sock.Accept(*cli_sock, &cli_addr)){
			continue;
		}

		cout << "new connect client: " << inet_ntoa(cli_addr.sin_addr)
			<< ", " << ntohs(cli_addr.sin_port) << endl;

		pthread_t tid;
		pthread_create(&tid, NULL, thr_start, (void*)cli_sock);
		pthread_detach(tid);
	}

	ret = sock.Close();
	if(!ret){
		return -1;
	}

	return 0;
}

如何判断TCP连接已经断开?

TCP的连接管理中,内建有保活机制。当长时间没有数据往来的时候每隔一段时间都会向对方发送一个保活探测包要求对方回复当多次发送的保活探测包都没有响应时,则认为连接断开
可通过命令查看sysctl -a | grep keepalive
连接断开,recv()函数会返回0,不是没有数据的意思,而是指连接断开。此时send()会触发异常(SIGPIPE)导致进程退出
注意:TCP是面向字节流的协议,有可能接收到半条数据,因此一定要对返回值进行判断,判断数据是否已经接收或完全接收

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值