一、Tcp Socket 常见的API介绍
相比于之前所学习的UDP,TCP是较为麻烦一下的,因为TCP协议比UDP协议要难一点,而且TCP需要先建立一条连接才能继续通信,而UDP是直接进行通信。下面来看一看,TCP要使用的API函数接口,顺便复习一下:
1.1 socket函数
函数的原型:
函数的功能:
socket
函数的主要功能是创建一个套接字,用于网络通信。创建的套接字可以用于各种网络操作,如连接到远程服务器、接收和发送数据等。成功创建的套接字用于建立和管理网络连接或进行数据传输。函数的参数:
domain
: 套接字的协议族。常见的值有:
AF_INET
:用于 IPv4 地址。AF_INET6
:用于 IPv6 地址。AF_UNIX
或AF_LOCAL
:用于本地进程间通信(IPC)。
type
: 套接字的类型,指定了套接字的传输方式。常见的值有:
SOCK_STREAM
:面向连接的套接字,使用 TCP 协议,提供可靠的数据传输。SOCK_DGRAM
:无连接的套接字,使用 UDP 协议,提供不可靠的数据传输。SOCK_RAW
:原始套接字,通常用于访问更底层的协议。
protocol
: 套接字所使用的协议。对于大多数应用,设置为0
表示自动选择合适的协议(通常是与type
相关的默认协议)。如果需要指定具体协议,可以用如下值:
IPPROTO_TCP
:与SOCK_STREAM
配合使用,表示使用 TCP 协议。IPPROTO_UDP
:与SOCK_DGRAM
配合使用,表示使用 UDP 协议。函数的返回值:
- 成功: 返回一个非负的整型文件描述符,表示创建的套接字。
- 失败: 返回 -1,并且设置
errno
以指示错误原因。常见的错误包括ENOMEM
(内存不足)、EAFNOSUPPORT
(地址族不支持)、EPROTONOSUPPORT
(协议不支持)等。
1.2 Bind函数
函数的原型:
函数的功能:
bind
函数将指定的地址(如 IP 地址和端口号)分配给一个已创建的套接字。对于服务器应用,这通常是设置套接字监听特定端口的步骤。函数的参数:
sockfd
: 套接字文件描述符,由socket
函数返回。addr
: 指向struct sockaddr
结构体的指针,包含要绑定的地址信息。addrlen
:addr
参数所指向的地址结构体的大小,以字节为单位。函数的返回值:
- 成功: 返回 0。
- 失败: 返回 -1,并设置
errno
以指示错误原因,如EADDRINUSE
(地址已在使用中)、EADDRNOTAVAIL
(地址不可用)等。
1.3 listen函数
函数的原型:
函数的功能:
listen
函数的主要功能是将套接字设置为被动模式,以便它可以接收传入的连接请求。调用listen
后,套接字会进入“监听”状态,等待客户端发起连接请求。函数的参数:
sockfd
: 套接字描述符。这个描述符是之前通过socket
函数创建的,并且在调用bind
函数后用来标识套接字。
backlog
: 指定在套接字上允许的最大未完成连接队列的长度。这个队列用于存放尚未被accept
函数接受的连接请求。当连接请求的数量超过这个值时,新连接请求可能会被拒绝或忽略。函数的返回值:
- 成功: 如果成功,返回
0
。- 失败: 如果调用失败,返回
-1
,并设置errno
以指示错误原因。常见的错误代码包括EBADF
(无效的套接字描述符)、ENOTSOCK
(描述符不是一个套接字)、EADDRINUSE
(地址已被使用)等。
1.4 accept函数
函数的原型:
函数的功能:
accept
函数的主要功能是从已监听的套接字中接受一个传入的连接请求,创建一个新的套接字用于与客户端进行通信。调用accept
函数后,服务器可以通过返回的新套接字与客户端交换数据。函数的参数:
sockfd
: 已监听的套接字描述符,即之前通过socket
函数创建并通过bind
和listen
函数设置好的套接字。
addr
: 指向sockaddr
结构体的指针,用于存储客户端的地址信息。如果不需要地址信息,可以传入NULL
。
addrlen
: 指向socklen_t
类型的变量的指针,该变量表示addr
参数所指向的地址结构体的大小。accept
函数会更新这个变量以反映实际的地址长度。如果addr
为NULL
,这个参数可以被忽略。函数的返回值:
成功: 返回一个新的套接字描述符,这个描述符用于与客户端进行通信。新套接字是与连接相关的独立套接字,且继承了原套接字的属性。
失败: 返回
-1
,并设置errno
以指示错误原因。常见的错误代码包括EBADF
(无效的套接字描述符)、EINTR
(调用被中断)、ENOTSOCK
(描述符不是一个套接字)、EOPNOTSUPP
(套接字类型不支持)等。
1.5 connect函数
函数的原型:
函数的功能:
connect
函数用于在客户端程序中与远程服务器建立连接。当客户端想要与服务器通信时,它使用connect
函数来请求连接。成功调用connect
后,客户端与服务器之间就可以进行数据传输了。函数的参数:
sockfd
: 这是一个套接字文件描述符,它是通过socket
函数创建的。这个套接字用于建立连接。
addr
: 这是一个指向struct sockaddr
结构体的指针,它包含了要连接的远程主机的地址信息。这个结构体的具体类型通常取决于协议族,例如struct sockaddr_in
用于 IPv4。
addrlen
: 这是addr
指向的地址结构的大小,以字节为单位。通常,可以使用sizeof(struct sockaddr_in)
来获得这个大小。函数的返回值:
- 成功: 返回 0 表示连接成功。
- 失败: 返回 -1,并且设置
errno
以指示错误原因。例如,errno
可能会被设置为ECONNREFUSED
表示目标主机拒绝连接,或ETIMEDOUT
表示连接超时等。
二、TcpServer代码的编写
我们先来看一看Tcp服务器编写的步骤:
- 创建TCP监听套接字
- 将TCP服务器的详细地址与TCP监听套接字进行绑定
- TCP服务器开启监听
- TCP服务器利用accept函数创建出新的套接字,通过新的套接字与客户端进行通信
2.1 TcpServer的初始化函数
void InitServer()
{
// 1. 创建套接字
_listensock = ::socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
// 创建失败
LOG(FATAL, "sockfd create failed!\n");
exit(SOCKET_ERROR);
}
LOG(INFO, "sockfd create success, sockfd is %d\n", _listensock);
// 2. 绑定套接字
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
int n = ::bind(_listensock, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
// 绑定失败
LOG(FATAL, "bind sockfd failed!\n");
exit(BIND_ERROR);
}
// 3. tcp是面向连接的,所以通信之前,需要先建立连接
// tcpserver启动,未来首先要一定等待用户的到来,需要进行监听
n = ::listen(_listensock, backlog);
if (n < 0)
{
// 监听失败
LOG(FATAL, "listen sockfd failed!\n");
exit(LISTEN_ERROR);
}
}
2.2 TcpServer的启动函数
void Loop()
{
// 4. 不能直接收数据,先获取连接
// accept在通信之前先获取客户端的地址, 返回值是文件描述符
// 这个文件描述符是什么?? 每建立一个链接就会有一个套接字, 用于IO操作
_isrunning = true;
while (_isrunning)
{
// 利用accept函数进行创建出新的套接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = ::accept(_listensock, (struct sockaddr *)&peer, &len);
if (sockfd < 0)
{
LOG(WARNING, "sockfd create failed!\n");
continue;
}
// // version 0
// Service(sockfd, InetAddr(peer)); // 一次只能处理一个请求
// // version 1 : 采用多进程
// pid_t id = fork();
// if (id == 0)
// {
// // 子进程
// // 子进程与父进程的文件描述表是独立的,子进程从父进程赋值一份,只关心sockfd
// ::close(_listensock); // 建议关掉
// if (fork() > 0)
// exit(0);
// Service(sockfd, InetAddr(peer)); // 孤儿进程
// exit(0);
// }
// // 父进程将新获取的文件描述交给子进程,只关心listensock
// ::close(sockfd);
// waitpid(id, nullptr, 0);
// // version 2 : 采用多线程
// pthread_t t;
// ThreadDate *td = new ThreadDate(sockfd, InetAddr(peer), this);
// pthread_create(&t, nullptr, handlerSock, td);
// // version 3 : 线程池技术
// test_t t = std::bind(&TcpServer::Service, this, sockfd, InetAddr(peer));
// ThreadPool<test_t>::GetInstance()->Enqueue(t);
}
_isrunning = false;
}
2.3 不同版本的Tcp服务器处理
void Service(int sockfd, InetAddr addr)
{
LOG(INFO, "tcpserver success...\n");
while (true)
{
char buffer[1024];
int n = ::read(sockfd, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
std::cout << buffer << std::endl;
std::string echo_string = "[server echo]";
echo_string += buffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if (n == 0)
{
// client关闭链接
LOG(DEBUG, "%s quit\n", addr.Ip());
break;
}
else
{
LOG(FATAL, "service failed!\n");
break;
}
}
::close(sockfd);
}
2.3.1 版本1——单线程版本
在这个版本中,我们只需要服务器进行读取客户端发来的数据,然后将读取到的结果返回给客户端。
Service(sockfd, InetAddr(peer)); // 一次只能处理一个请求
2.3.2 版本2——采用多进程版本
在这个版本中,我们需要利用fork函数创建出子进程,让子进程去处理业务,但是这样,父进程需要等待回收子进程,这样会进行阻塞;所以在这里,我们需要在子进程中再次调用fork函数创建出子进程,并将父进程进行退出,使得子进程变成孤儿进程,交给bash管理,最后将服务处理交给“孙子进程”。
// version 1 : 采用多进程
pid_t id = fork();
if (id == 0)
{
// 子进程
// 子进程与父进程的文件描述表是独立的,子进程从父进程赋值一份,只关心sockfd
::close(_listensock); // 建议关掉
if (fork() > 0)
exit(0);
Service(sockfd, InetAddr(peer)); // 孤儿进程
exit(0);
}
// 父进程将新获取的文件描述交给子进程,只关心listensock
::close(sockfd);
waitpid(id, nullptr, 0);
2.3.3 版本3——采用多线程
在这个版本中,我们所使用的是自己定义的线程函数,我们需要定义出ThreadDate结构体,使用线程创建函数创建出新线程,将任务处理交给新线程,并将新线程分离。
// ThreadDate结构体
class ThreadDate
{
public:
ThreadDate(int sockfd, InetAddr addr, TcpServer *s) : _sockfd(sockfd), _addr(addr), self(s) {}
InetAddr _addr;
int _sockfd;
TcpServer *self;
};
static void *HandlerSock(void *args)
{
pthread_detach(pthread_self());
ThreadDate *td = static_cast<ThreadDate *>(args);
td->self->_service(td->_sockfd, td->_addr);
delete td;
return nullptr;
}
// version 2 : 采用多线程
pthread_t t;
ThreadDate *td = new ThreadDate(sockfd, InetAddr(peer), this);
pthread_create(&t, nullptr, handlerSock, td);