文章目录
- TCP模型的特性
- TCP接口介绍
- TCP服务器套接字设置
- TCP客户端套接字设置
TCP模型的特性
TCP是属于传输层协议的一种,上篇博客介绍了另一种传输层协议——UDP,关于它们之间的区别,这里再提一下
TCP | UDP |
---|---|
传输层协议 | 传输层协议 |
有连接 | 无连接 |
可靠连接 | 不可靠连接 |
面向字节流 | 面向数据报 |
其中更详细的区别在UDP套接字编程中已经讲解,这里不再赘述,直接进入TCP相关接口的介绍
TCP接口介绍
listen:使socket文件处于监听状态,此时网络中的设备可以向处于监听状态的套接字文件发送连接请求,backlog则是套接字文件的监听队列的长度。因为TCP协议是面向连接的协议,在通信之前需要双方确定连接,所以服务端通常需要先进入监听状态等待其他设备的连接,建立了连接才能进行通信。
accept:刚才的listen使一个socket文件处于监听状态,accept的作用就是接受socket的监听队列中的第一个套接字文件的连接,并且会创建一个服务套接字,服务套接字的套接字类型与协议家族和监听套接字的相同(端口号肯定不同),服务器用socket监听套接字监听网络中的连接请求,用accept返回的套接字文件为请求套接字提供服务。两个套接字文件都用来通信,一个是监听套接字,以通信的方式监听网络中的请求,一个是服务套接字,以通信的方式为发送请求的客户端提供服务。
参数解释:
socket:监听套接字的文件描述符
address:输出型参数,接受连接的套接字信息结构体
address_len:输出型参数,套接字信息的结构体的字节大小
所以accept可以接受监听队列中的连接请求,并以输出型参数的形式保存请求方的套接字信息,最后accept会返回一个套接字文件fd,用来为请求方提供服务。因此,只要accept了一次请求,就会有一个套接字文件被创建以提供服务,如果请求很多,用来提供服务的套接字相应的也会有很多,但是一台设备上的文件数是有上限的,如果对请求方提供的服务完成,没有及时关闭提供服务的套接字文件,就会造成文件资源的泄漏,最终造成服务器的崩溃,所以在编写TCP通信的代码时,一定要记住:当服务完成,用于提供服务的套接字文件必须及时关闭。
如果accept调用失败,函数会返回-1并设置错误码
补充两个IP地址转换的接口,一个是inet_aton,将字符串形式的IP转换成一个32位无符号整数
cp:是要转换字符串IP的起始地址
inp:是转换后的IP存储的地址
可以看到转换后的inp也是一个地址,并且其类型是struct in_addr*
这类型不就对应了struct sockaddr_in的成员struct in_addr吗?原本需要直接对struct in_addr的成员in_addr_t类型的s_addr直接赋值,但是使用inet_aton函数就不需要了,该函数直接返回一个类型为struct in_addr的结构体,里面包含了以32位无符号整数存储的IP地址
struct sockaddr_in local;
_ip.empty() ? local.sin_addr.s_addr = INADDR_ANY : inet_aton(_ip.c_str(), &local.sin_addr);
(INADDR_ANY表示绑定该主机上的所有ip地址,只要网络中的设备发送的ip地址是该主机的其中一个,该主机就可以接受到)
还有一个接口inet_ntoa,可以将以32位整数形式表示的ip地址转换成点分十进制字符串形式的ip地址
inet_ntoa的形参有点特殊,不是一个32位整数,而是一个结构体struct in_addr,该结构体是struct sockaddr_in结构体下的一个成员sin_addr的类型
struct sockaddr_in local;
char ip[] = inet_ntoa(local.sin_addr);
// ip数组就是以点分十进制表示的ip地址
connect:连接到一个socket文件,通常用于客户端与服务器的连接
socket:自己的socket文件fd,用自己的socket文件与服务器的socket文件进行通信
address:需要连接的套接字文件信息结构体
address_len:address结构体的字节大小
也就是说客户端使用connect与服务器连接,但是需要先知道服务器的套接字信息,创建了自己的套接字,有了服务器的套接字信息,就可以与服务器进行连接了
TCP服务器套接字设置
// util.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <strings.h>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/stat.h>
#define SOCK_FAIL 1
#define BIND_FAIL 2
#define LSTE_FAIL 3
#define CONN_FAIL 4
#define USAG_ERRO 5
#include "util.hpp"
#include <signal.h>
class tcpServer
{
public:
tcpServer(uint16_t port, std::string ip = "") : _ip(ip), _port(port) {}
~tcpServer() {}
void init()
{
// 创建套接字文件
_listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listen_sockfd < 0)
{
std::cerr << "socket: fail" << std::endl;
exit(SOCK_FAIL);
}
// 填充套接字信息
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
_ip.empty() ? local.sin_addr.s_addr = INADDR_ANY : inet_aton(_ip.c_str(), &local.sin_addr);
// 将信息绑定到套接字文件中
if (bind(_listen_sockfd, (const struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind: fail" << std::endl;
exit(BIND_FAIL);
}
// 至此,套接字创建完成,所有的步骤与udp通信一样
// 使套接字进入监听状态
if (listen(_listen_sockfd, 5) < 0)
{
std::cerr << "listen: fail" << std::endl;
exit(LSTE_FAIL);
}
// 套接字初始化完成
std::cout << "listen done" << std::endl;
}
void loop()
{
signal(SIGCHLD, SIG_IGN); // 设置SIGCHLD信号为忽略,子进程会自动释放资源
// 创建保存套接字信息的结构体
struct sockaddr_in peer;
socklen_t peer_len = sizeof(peer);
// 接受监听队列中的套接字请求
while (1)
{
int server_sockfd = accept(_listen_sockfd, (struct sockaddr *)&peer, &peer_len);
if (server_sockfd < 0)
{
std::cerr << "accept: fail" << std::endl;
continue;
}
std::cout << "accept done" << std::endl;
// 提取请求方的套接字信息
uint16_t peer_port = ntohs(peer.sin_port);
std::string peer_ip = inet_ntoa(peer.sin_addr);
// 打印请求方的套接字信息
std::cout << "accept: " << peer_ip << " [" << peer_port << "]" << std::endl;
// 提供服务
pid_t id = fork();
if (id == 0)
{
// child
service(server_sockfd);
}
}
}
void service(int server_sockfd)
{
char in_buffer[1024];
while (1)
{
ssize_t ret = read(server_sockfd, in_buffer, sizeof(in_buffer));
if (ret > 0)
{
in_buffer[ret] = '\0';
if (strcasecmp("quit", in_buffer) == 0)
{
std::cout << "client quit, service done" << std::endl;
break;
}
// 假设服务是在client发送的字符串后添加一串字符串
strcat(in_buffer, ",service done");
// 服务完成,将字符串发送给client
write(server_sockfd, in_buffer, sizeof(in_buffer));
}
else if (ret == 0)
{
std::cout << "client quit, service done" << std::endl;
break;
}
else
{
std::cout << "service fail" << std::endl;
break;
}
}
close(server_sockfd);
}
private:
std::string _ip;
uint16_t _port;
int _listen_sockfd;
};
int main()
{
tcpServer server(8080);
server.init();
server.loop();
return 0;
}
封装tcpServer类,该类的成员_ip保存了服务器的IP地址,_port保存了端口号,以及_listen_sockfd保存了监听套接字的文件描述符。tcpServer的构造就是初始化这些成员,如果用户没有传入指定的IP地址,那么tcpServer的成员_ip就是一个空字符串,在绑定套接字信息时,会将该主机上的所有IP绑定到套接字文件中(INADDR_ANY)。
tcpServer的初始化函数:init,init会调用socket接口,创建一个套接字(打开一个套接字文件),然后填充套接字信息到struct sockaddr_in结构体中,接着调用bind绑定套接字信息到套接字文件中,至此TCP套接字的初始化和UDP通信一样,UDP通信的服务器此时已经初始化完成,可以调用recvfrom接收来自客户端的信息了。这是无连接的UDP通信,而TCP通信是面向连接的,所以在通信之前需要确定双方的连接,具体表现为:服务器调用listen进入监听状态,客户端调用connect连接处于监听状态的服务器。所以tcpServer的init初始化的套接字被用来监听网络中的连接请求,是客户端与服务器之间连接的窗口。因此,init总体分为四步,创建套接字,填充套接字信息,绑定套接字,使套接字处于监听状态,init完成,服务器就处于监听状态,可以接收网络中的连接请求。
服务器初始化后,就需要为客户提供服务,设置loop函数使服务器对外提供服务:先调用accept接收客户端向监听套接字发送的连接请求,由于accept会创建一个新的套接字并返回其文件描述符,所以我们需要接收accept的返回值,拿到新创建的套接字文件,用新的套接字文件为客户端提供服务。因此,在TCP模型中有两个套接字,一个监听套接字,一个服务套接字,监听套接字是连接服务器与客户端的窗口,服务套接字是服务器为客户端提供服务的窗口。只要服务器接受了监听套接字监听到的连接请求,服务器就需要创建一个服务套接字,通过服务套接字为客户端提供服务。但是服务器是一个进程,所以服务器提供服务时只能一对一的提供,服务完成一个客户再服务下一个客户,很显然,这样一个一个的服务效率太低,所以我们可以创建子进程,使子进程通过服务套接字为客户端提供服务。
但是当子进程服务完成,子进程就会退出,此时的子进程处于僵尸状态,父进程需要回收子进程的资源,如果不回收子进程就会引起资源泄漏的问题。所以父进程需要调用waitpid回收子进程,但是父进程又不能阻塞式的等待子进程的退出,阻塞式的等待与服务器一个个的服务客户没有区别,所以父进程就需要非阻塞式的等待子进程,但是父进程就需要记录所有子进程的pid,这样也有点麻烦,最简单的方法就是将SIGCHLD信号的handler设置为SIG_IGN,使子进程在退出时自动释放自己的资源(但这种方法只有在Linux下有效)。
除了创建子进程,使子进程为客户提供服务的做法,我们还可以创建孙子进程,使父进程退出。就是说,现在的服务器是一个祖父进程,服务器创建了一个子进程,接着在这个子进程中再创建一个子进程,那么服务就创建了两个子进程,它们的关系是祖父进程,父进程与子进程,我们可以使父进程退出,那么子进程成为孤儿进程,由1号进程托管,对客户端提供的服务由子进程完成,当子进程退出时,1号进程就会自动释放它的资源,这样它就不会处于僵尸状态,造成资源的泄漏了。但是祖父进程就需要阻塞式的等待回收父进程(当父进程创建完子进程后,父进程就会退出,可以说一瞬间父进程就退出了,祖父进程也不会等待太久),不然父进程就会进入僵尸,又造成资源泄漏。
除了创建孙子进程这样的操作,我们还能创建多线程,但是我们不能join新线程,因为join会造成进程的阻塞,我们只能将线程分离,使其退出时自动释放资源
TCP客户端套接字设置
#include "util.hpp"
void usage(const char *filename)
{
std::cout << "usage:\n\t"
<< filename << "IP port" << std::endl;
}
volatile bool quit = false;
int main(int argc, char* argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(USAG_ERRO);
}
std::string server_ip = argv[1];
uint16_t server_port = atoi(argv[2]);
// 服务器套接字的填充
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
// 套接字的创建
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
std::cerr << "socket: fail" << std::endl;
exit(SOCK_FAIL);
}
// 与服务器的连接
if (connect(sockfd, (const struct sockaddr*)&server, sizeof(server)) < 0)
{
std::cerr << "connect: fail" << std::endl;
exit(CONN_FAIL);
}
std::cout << "connect done" << std::endl;
while (!quit)
{
std::cout << "Please Enter#";
char in_buffer[1024] = {0};
char out_buffer[1024] = {0};
std::cin.getline(in_buffer, sizeof(in_buffer));
// std::cout << in_buffer << std::endl;
if (strcasecmp("quit", in_buffer) == 0)
{
// 注意不要退出,让客户端向服务器发送quit,服务器接收quit将关闭服务
quit = true;
}
ssize_t w_ret = write(sockfd, in_buffer, sizeof(in_buffer));
if (w_ret > 0)
{
ssize_t r_ret = read(sockfd, out_buffer, sizeof(out_buffer));
if (r_ret > 0)
{
out_buffer[r_ret] = '\0';
std::cout << "receive: " << out_buffer << std::endl;
}
else
{
std::cerr << "read: fail" << std::endl;
break;
}
}
else
{
std::cerr << "write: fail" << std::endl;
break;
}
}
return 0;
}
客户端要做的就是:填充服务器的套接字信息,并创建自己的套接字,最重要的就是调用connect用自己的套接字连接服务器的监听套接字,也就是发送与服务器的连接请求,当服务器调用listen处于监听状态时,客户端的连接才能发送成功,由于TCP协议下发送的数据是流式的,所以我们可以使用write和read向套接字文件中读写信息(UDP的数据是数据报式的,recvfrom和sendto是用来发送数据报式数据的接口)