一、socket编程接口
1.socket函数
2.connect函数
3.bind函数
4.listen函数
5.accept函数
6.sockaddr结构
二、封装tcp socket
三、TCP通用服务器 tcpserver.hpp
套接字接口是一组函数,他们和Unix IO结合起来,用以创建网络应用。从linux内核来看,一个套接字就是通信的一个断点。从linux应用程序看,套接字就是一个有相应描述符的打开文件。
因特网的套接字地址存放在sockaddr_in的16字节结构中,对于因特网,sin_family是AF_INET,sin_port成员是一个16位的端口号,sin_addr就是一个32位的ip地址,ip和port都是以网络字节顺序(大端)存储的。
connect,bind,accept函数要求一个指向与协议相关的套接字地址结构的指针。
客户端和服务器使用socket来创建一个套接字描述符
//创建socket 文件描述符
#include
#include
int socket(int domain,int type,int protocol);
//使用示例
clientfd = socket(AF_INET,SOCK_STREAM,0);
客户端通过connect函数来建立和服务器的连接
//建立连接
int connect(int sockfd,const struct sockaddr* addr,socklen_t addrlen);
connect函数试图与套接字地址为addr的服务器建立一个因特网连接,其中addrlen是sizeof(sockaddr_in)。connect函数会阻塞,一直到连接成功建立,或者发生错误。如果成功连接,clientfd描述符现在就准备好可以读写了,并且得到的连接是对(x:y,addr.sin_addr:addr.sin_port)刻画的,其中x表示客户端的ip地址,y表示临时端口,它唯一地确定了客户端主机上的客户端进程。
服务器使用bind/listen/accept来和客户端建立连接
//绑定端口号
int bind(int socket,const struct sockaddr* addr, socklen_t address_len);
bind函数告诉内核将addr中的服务器套接字地址和套接字描述符fd联系起来,len是sizeof(addr)
客户端是发起连接请求的主动实体,服务器是等待来自客户端的连接请求的被动实体。默认情况下,内核会认为socket函数创建的描述符对于主动套接字(active-socket),它存在于一个连接的客户端。服务器调用listen告诉内核,描述符是被服务器使用的而不是客户端使用的。
//开始监听socket
int listen(int socket,int backlog);
listen函数将sockfd从一个主动套接字转换成一个监听套接字,该套接字可以接受来自客户端的连接请求。backlog参数暗示了内核在开始拒绝连接请求之前,队列中要排队的未完成的连接请求的数量。backlog一般通常设置为一个较大的数值,如1024
//接收请求
int accept(int listenfd,struct sockaddr* addr,socklen_t address_len);
accept函数等待来自客户端的连接请求到达侦听描述符listenfd,然后在addr中填写客户端的套接字地址,并且返回一个socket fd,这个返回值用于与客户端通信。
listenfd是作为客户端请求的一个端点,通常被创建一次,存在于服务器的整个生命周期。socketfd是客户端和服务器之间已经建立起来了连接的一个断点,服务器每次接收连接请求时都会被创建一次,只存在于服务器为一个客户端的服务过程中。
客户端实现socket套接字创建,connect和服务器端连接。服务器端完成socket,bind,listen,accept与客户端完成通信。listenfd和socketfd区别开,它可以使得我们建立并发服务器,能够处理多个客户端连接。例如每次客户端发一个connect到listenfd,我们可以fork一个新的进程,通过connectfd与客户端进行通信。
socket API是一层抽象的网络编程接口,适用于各种底层网络协议ipv4,ipv6等,然而,各种网络协议的地址格式并不相同。
ipv4/ipv6的地址格式定义在netinet/in.h中,ipv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位ip地址ipv4/ipv6地址类型分别定义位常数AF_INET 、AF_INET_6,只要取得某种sockaddr的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容
sockaddr api都可以用struct sockaddr*表示,在使用的时候强制转换为sockaddr_in,这样的好处是程序的通用性,可以接收ipv4,ipv6以及unix domain socket各种类型的sockaddr结构体指针作为参数
虽然socket api中使用的接口是sockaddr,但是真正基于ipv4编程的时候,使用的数据结构是sockaddr_in,这个结构体主要有三部分信息:地址类型,端口号,ip地址
其中in_addr用来表示一个ipv4的ip地址,其实就是一个32位的整数
class TcpSocket
{
public:
TcpSocket() : fd_(-1) { }
TcpSocket(int fd) : fd_(fd) { }
bool Socket()
{
fd_ = socket(AF_INET, SOCK_STREAM, 0);
if (fd_ < 0)
{
perror("socket");
return false;
}
printf("open fd = %d\n", fd_);
return true;
}
bool Close() const
{
close(fd_);
printf("close fd = %d\n", fd_);
return true;
}
bool Bind(const std::string& ip, uint16_t port) const
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip.c_str());
addr.sin_port = htons(port);
int ret = bind(fd_, (sockaddr*)&addr, sizeof(addr));
if (ret < 0) { perror("bind"); return false;}
return true;
}
bool Listen(int num) const
{
int ret = listen(fd_, num);
if (ret < 0) { perror("listen"); return false; }
return true;
}
bool Accept(TcpSocket* peer, std::string* ip = NULL, uint16_t* port = NULL) const
{
sockaddr_in peer_addr;
socklen_t len = sizeof(peer_addr);
int new_sock = accept(fd_, (sockaddr*)&peer_addr, &len);
if (new_sock < 0) {perror("accept");return false;}
printf("accept fd = %d\n", new_sock);
peer->fd_ = new_sock;
if (ip != NULL) { *ip = inet_ntoa(peer_addr.sin_addr);}
if (port != NULL) {*port = ntohs(peer_addr.sin_port);}
return true;
}
bool Recv(std::string* buf) const
{
buf->clear();
char tmp[1024 * 10] = {0};
ssize_t read_size = recv(fd_, tmp, sizeof(tmp), 0);
if (read_size < 0) {perror("recv");return false;}
if (read_size == 0) {return false;}
buf->assign(tmp, read_size);
return true;
}
bool Send(const std::string& buf) const
{
ssize_t write_size = send(fd_, buf.data(), buf.size(), 0);
if (write_size < 0) {perror("send"); return false;}
return true;
}
bool Connect(const std::string& ip, uint16_t port) const
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip.c_str());
addr.sin_port = htons(port);
int ret = connect(fd_, (sockaddr*)&addr, sizeof(addr));
if (ret < 0) {perror("connect");return false;}
return true;
}
int GetFd() const {return fd_;}
private:
int fd_;
};
#pragma once
#include
#include
#include
#include
#include /* See NOTES */
#include
#include
#include
#include
#include
#include
#include
#include
#include "err.hpp"
namespace ns_server
{
static const uint16_t defaultport = 8081;
static const int backlog = 32; //? TODO
using func_t = std::function;
class TcpServer;
class ThreadData
{
public:
ThreadData(int fd, const std::string &ip, const uint16_t &port, TcpServer *ts)
: sock(fd), clientip(ip), clientport(port), current(ts)
{}
public:
int sock;
std::string clientip;
uint16_t clientport;
TcpServer *current;
};
class TcpServer
{
public:
TcpServer(func_t func, uint16_t port = defaultport) : func_(func), port_(port), quit_(true)
{
}
void initServer()
{
// 1. 创建socket, 文件
listensock_ = socket(AF_INET, SOCK_STREAM, 0);
if (listensock_ < 0)
{
std::cerr << "create socket error" << std::endl;
exit(SOCKET_ERR);
}
// 2. bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port_);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listensock_, (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind socket error" << std::endl;
exit(BIND_ERR);
}
// 3. 监听
if (listen(listensock_, backlog) < 0)
{
std::cerr << "listen socket error" << std::endl;
exit(LISTEN_ERR);
}
}
void start()
{
// signal(SIGCHLD, SIG_IGN); //ok, 我最后推荐的!
// signal(SIGCHLD, handler); // 挥手,不太推荐
quit_ = false;
while (!quit_)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
// 4. 获取连接,accept
int sock = accept(listensock_, (struct sockaddr *)&client, &len);
if (sock < 0)
{
std::cerr << "accept error" << std::endl;
continue;
}
// 提取client信息 -- debug
std::string clientip = inet_ntoa(client.sin_addr);
uint16_t clientport = ntohs(client.sin_port);
// 5. 获取新连接成功, 开始进行业务处理
std::cout << "获取新连接成功: " << sock << " from " << listensock_ << ", "
<< clientip << "-" << clientport << std::endl;
// v1
// service(sock, clientip, clientport);
// v2: 多进程版本
// pid_t id = fork();
// if (id < 0)
// {
// close(sock);
// continue;
// }
// else if (id == 0) // child, 父进程的fd,会被child继承吗?会。 父子会用同一张文件描述符表吗?不会,子进程拷贝继承父进程的fd table;
// {
// // 建议关闭掉不需要的fd
// close(listensock_);
// if(fork() > 0) exit(0); // 就这一行代码
// // child已经退了,孙子进程在运行
// service(sock, clientip, clientport);
// exit(0);
// }
// // 父, 一定关闭掉不需要的fd, 不关闭,会导致fd泄漏
// close(sock);
// pid_t ret = waitpid(id, nullptr, 0); //阻塞的! waitpid(id, nullptr, WNOHANG);//不推荐
// if(ret == id) std::cout << "wait child " << id << " success" << std::endl;
// v3: 多线程 -- 原生多线程
// 1. 要不要关闭不要的socket??不能
// 2. 要不要回收线程?如何回收?会不会阻塞??
// pthread_t tid;
// ThreadData *td = new ThreadData(sock, clientip, clientport, this);
// pthread_create(&tid, nullptr, threadRoutine, td);
// v4: 一旦用户来了,你才创建线程, 线程池吗??
}
}
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast(args);
td->current->service(td->sock, td->clientip, td->clientport);
delete td;
return nullptr;
}
void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
std::string who = clientip + "-" + std::to_string(clientport);
char buffer[1024];
while (true)
{
ssize_t s = read(sock, buffer, sizeof(buffer) - 1); // recv TODO
if (s > 0)
{
buffer[s] = 0;
std::string res = func_(buffer); // 进行回调
std::cout << who << ">>> " << res << std::endl;
write(sock, res.c_str(), res.size());
}
else if (s == 0)
{
// 对方将连接关闭了
close(sock);
std::cout << who << " quit, me too" << std::endl;
break;
}
else
{
close(sock);
std::cerr << "read error: " << strerror(errno) << std::endl;
break;
}
}
}
~TcpServer()
{
}
private:
uint16_t port_;
int listensock_; // TODO
bool quit_;
func_t func_;
};
}