一、常见网络接口总结
1、创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
domain:AF_INET:网络通信,AF_LOCAL:本地通信
type:UDP:SOCK_DGRAM,TCP:SOCK_STREAM
protocol:协议编号一开始设0
返回值:文件描述符,Linux下一切皆文件
2、绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
socket:创建socket文件描述符的返回值
address:输入型参数,传递一个带有服务器信息(IP, 端口号, AF_INET)的结构体对象,具体详见http://t.csdnimg.cn/aY2O0
address_len:结构体 address 大小
3、开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
将套接字设为监听状态,随时准备别人链接服务器
backlog:表示大小,设为8
4、接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
服务器监听状态之后,接受客户端链接
address:输出型参数,得到客户端的信息
address_len:输出型参数,address大小
返回值:文件描述符
对比 accept 函数返回值和 socket
就好比一家餐馆,socket 是门口拉客的,一般叫 listen_sockfd 监听套接字。返回值是客人进餐馆接待客人的服务员,为用户提供网络IO的服务套接字。所以之后真正开始服务的函数用的是accept函数返回值
5、建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
客户端在有需要时进行链接服务器
address:输入型参数,传入带有服务器信息的结构体,表示我要链接你这个服务器。
address_len:结构体 address 大小
6、把字符串ip转化成网络序列ip
in_addr_t inet_addr(const char *cp);(不安全)
int inet_pton(int af, const char *src, void *dst);(推荐)
dst:直接填 struct sockaddr_in 中的 sin_addr
7、把网络序列ip转化成字符串ip
char *inet_ntoa(struct in_addr in);(不安全)内部只维护一块静态空间存储IP,会导致覆盖问题
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);(推荐)
af:AF_INET:网络通信,AF_LOCAL:本地通信
src:4字节网络序列IP
dst:传入自己定义的缓冲区存放ip
size:缓冲区大小
8、从网络中收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
用于tcp读取数据
flag:设置0,阻塞读取
由于tcp有链接,且sockfd本质是文件描述符,可以用read函数
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
用于udp读取数据
src_addr:输出型参数,客户端信息
addrlen:输出型参数,src_addr大小
9、在网络中发数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
用于tcp发数据
flag:设置0,阻塞发送
由于tcp有链接,且sockfd本质是文件描述符,可以用write函数
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
用于udp发数据
dest_addr:输入型参数,服务器信息
addrlen:dest_addr大小
10、库函数做网络字节序和主机字节序的转换接口
二、udp_echo_server
1、服务器初始化服务
cpp
void InitServer()
{
// 1.创建socket文件描述符 一般是3
_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd < 0)
{
LOG(FATAL, "socket error\n");
exit(SOCKET_ERROR);
}
LOG(DEBUG, "socket create success, _sockfd = %d\n", _sockfd);
// 2.先填充跨网络地址信息
struct sockaddr_in local;
memset(&local, 0, sizeof local);
// 通信方式
local.sin_family = AF_INET;
// 主机转成网络序列
local.sin_port = htons(_port);
// 要4字节ip 要网络序列ip
// local.sin_addr.s_addr = inet_addr(_localip.c_str());
// 服务器端进行任意IP绑定
local.sin_addr.s_addr = INADDR_ANY;
// 3.绑定端口号 把地址信息绑定进套接字
int n = ::bind(_sockfd, (struct sockaddr*)&local, sizeof local);
if(n < 0)
{
LOG(FATAL, "bind error\n");
exit(BIND_ERROR);
}
LOG(DEBUG, "bind success\n");
}
(1)创建sockfd文件描述符
(2)为了绑定函数,初始化 struct socket_in server,并填入服务器信息
注意:服务器一定要绑定 INADDR_ANY 任意IP,这样才能接收到全部IP地址主机的数据
(3)绑定端口号以及一系列服务器信息
2、服务器启动服务
cpp
// 绑定成功就开始服务
void Start()
{
_isrunning = true;
char inbuffer[1024];
// 死循环
while(_isrunning)
{
struct sockaddr_in peer;
socklen_t len = sizeof peer;
// 服务器收客户端消息
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof inbuffer - 1, 0, (struct sockaddr*)&peer, &len);
if(n > 0)
{
// 获取哪一个客户端的端口号和IP地址
InetAddr addr(peer);
inbuffer[n] = 0;
cout << "[" << addr.Ip() << ":" << addr.Port() << "]#" << inbuffer << endl;
string echo_string = "[udp_server echo] #";
echo_string += inbuffer;
// 服务器发给客户端消息
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&peer, len);
}
}
}
至少服务器要一直死循环工作
3、客户端初始化
cpp
// 1.获取服务器IP和端口号
string server_ip = argv[1];
uint16_t server_port = stoi(argv[2]);
// 2.绑定服务器
// 绑定
// 客户端端口号一般不让用户自己设定,让客户端OS随机选择端口
// 客户端一定需要绑定自己的IP地址和端口号,但不是显示绑定
// 客户端在首次向服务器发送数据时,OS自动给客户端绑定IP和端口号
struct sockaddr_in server;
memset(&server, 0, 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_DGRAM, 0);
if(sockfd < 0)
{
cerr << "create socket error" << endl;
exit(1);
}
(1)获取要连接的服务器信息
(2)初始化 struct socket_in server,并填入服务器信息
(3)创建套接字
(4)重点
客户端不需要显示绑定端口号,也不能自己绑定。但是不代表客户端不要绑定,在第一次客户端给服务器发数据时让客户端的OS随机绑定。
为什么不让用户自己绑定?
服务器可以自己绑定是因为服务器一般只运行一个进程服务,端口号不会冲突,但是客户端不一样,用户的操作系统上会有多个客户端,如果让用户自己设置就很可能端口号冲突,导致用户不能同时启动两个端口号一样的客户端。
4、客户端链接服务器
cpp
while(1)
{
//发送的数据
string line;
cout << "Please Enter# : ";
getline(cin, line);
int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr*)&server, sizeof server);
if(n > 0)
{
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
int m = recvfrom(sockfd, buffer, sizeof buffer - 1, 0, (struct sockaddr*)&temp, &len);
if(m > 0)
{
buffer[m] = 0;
cout << buffer << endl;
}
else
break;
}
else
break;
}
三、tcp_echo_server
1、服务器初始化服务
cpp
void InitServer()
{
// 1.创建socket
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if(_listensockfd < 0)
{
LOG(FATAL, "socket create error\n");
exit(SOCKET_ERROR);
}
LOG(INFO, "socket create success, sockfd: %d\n", _listensockfd);
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;
// 2.绑定
if(::bind(_listensockfd, (struct sockaddr*)&local, sizeof local) < 0)
{
LOG(FATAL, "socket bind error\n");
exit(BIND_ERROR);
}
LOG(INFO, "bind success\n");
// 3.因为tcp是面向连接的,tcp需要不断能获取链接状态,设置为listen状态
if(::listen(_listensockfd, gblcklog) < 0)
{
LOG(FATAL, "listen error\n");
exit(LISTEN_ERROR);
}
LOG(INFO, "listen success\n");
}
(1)创建sockfd文件描述符
(2)为了绑定函数,初始化 struct socket_in server,并填入服务器信息
注意:服务器一定要绑定 INADDR_ANY 任意IP,这样才能接收到全部IP地址主机的数据
(3)绑定端口号以及一系列服务器信息
(4)开始监听
2、服务器启动服务
cpp
// 内部类 为了拿到新线程要用的sockfd和this指针调用Service函数
class ThreadData
{
public:
int _sockfd;
TcpServer* _self;
InetAddr _addr;
public:
ThreadData(int sockfd, TcpServer* p, const InetAddr &addr)
:_sockfd(sockfd)
,_self(p)
,_addr(addr)
{}
};
void Loop()
{
// 推荐做法 忽略子进程退出信息
// signal(SIGCHLD, SIG_IGN);
_isrunning = true;
struct sockaddr_in client;
socklen_t len = sizeof client;
while(_isrunning)
{
// 4.获取链接
int sockfd = accept(_listensockfd, (struct sockaddr*)&client, &len);
// listensockfd获取失败
if(sockfd < 0)
{
LOG(WARNING, "accept error\n");
continue;
}
// 获取成功
InetAddr addr(client);
LOG(INFO, "get a new link, client info: %s\n", addr.AddStr().c_str());
// 版本一 无法并发访问服务器
// Service(sockfd, addr);
// 版本二 多进程版并发访问服务器
// pid_t id = fork();
// if(id == 0)
// {
// // 继承下来的父进程的_listensockfd(3)需要在子进程关闭,子进程使用sockfd(4)
// ::close(_listensockfd);
// // 创建孙子进程后,子进程退出,此时孙子进程成为孤儿进程,执行服务,死活只由OS关心
// if(fork() > 0)
// {
// exit(0);
// }
// Service(sockfd, addr);
// exit(0);
// }
// // 同理父进程也要关闭sockfd(4)因为那是属于子进程的,用_listensockfd(3)
// // 必须要释放子进程的sockfd, 防止文件描述符泄漏
// int n = waitpid(id, nullptr, 0);
// if(n > 0)
// {
// LOG(INFO, "wait chid success\n");
// }
// 版本三 多线程版并发访问服务器
// pthread_t tid;
// ThreadData* td = new ThreadData(sockfd, this, addr);
// // 新线程内部分离线程
// pthread_create(&tid, nullptr, Excute, td);
// 版本四 线程池版本
task_t t = bind(&TcpServer::Service, this, sockfd, addr);
ThreadPool<task_t>::GetInstance()->Equeue(t);
}
_isrunning = false;
}
// 类里面方法带有this指针,标为静态
// 多线程因为共用同一个文件描述符表,所以不能关闭
static void* Excute(void* args)
{
pthread_detach(pthread_self());
ThreadData* td = static_cast<ThreadData*>(args);
td->_self->Service(td->_sockfd, td->_addr);
delete(td);
return nullptr;
}
void Service(int sockfd, InetAddr addr)
{
while(1)
{
char inbuffer[1024];
ssize_t n = read(sockfd, inbuffer, sizeof inbuffer - 1);
if(n > 0)
{
inbuffer[n] = 0;
string echo_string = "[server echo] #";
echo_string += inbuffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if(n == 0)
{
LOG(INFO, "client %s quit\n", addr.AddStr().c_str());
break;
}
else
{
LOG(ERROR, "read error: %s\n", addr.AddStr().c_str());
break;
}
}
::close(sockfd);
}
(1)accpet 函数循环获取新连接,获取成功填入客户端信息
(2)服务端提供服务的四种方式
a、直接调用服务函数
不能并发访问服务器,不使用
b、多进程调用服务函数
创建子进程后,父进程关闭子进程套接字 sockfd,子进程关闭父进程套接字 _listen_sockfd,让父子进程不能相互影响。
signal(SIGCHLD, SIG_IGN); 忽略子进程退出,父进程不用阻塞等待。
也可以在子进程中创建孙子进程,子进程退出,孙子进程变成孤儿进程执行服务函数,函数结束不用父进程管,让OS管。还是推荐忽略子进程退出的方式。
c、多线程调用服务函数
为了线程创建函数中的参数线程调用函数不能有this指针,把调用函数设 static
多线程使用同一张文件描述符表,不能关闭任何套接字
要定义内部类,给线程执行函数传入 sockfd, this指针(调用Service函数), addr
新线程在执行函数中要 pthread_detach(pthread_self()) 进行线程分离
d、线程池调用服务函数
using task_t = function<void()>;
task_t t = bind(&TcpServer::Service, this, sockfd, addr);
ThreadPool<task_t>::GetInstance()->Equeue(t);
3、客户端初始化
cpp
if (argc != 3)
{
cerr << "Usage: " << argv[0] << " server_ip server_port" << endl;
exit(0);
}
string server_ip = argv[1];
uint16_t server_port = stoi(argv[2]);
// 1.创建socket
int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
cerr << "create socket error" << endl;
exit(1);
}
// 注意:客户端不需要显示绑定,但一定要有自己的ip和port,OS会自动绑定sockfd, 用自己的ip和随机端口号
// 向服务器发送链接(第一次发送链接时,OS自动绑定)
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
::inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);
(1)获取要连接的服务器信息
(2)初始化 struct socket_in server,并填入服务器信息
(3)创建套接字
4、客户端链接服务器
cpp
int n = ::connect(sockfd, (struct sockaddr*)&server, sizeof server);
if(n < 0)
{
cerr << "connect error" << endl;
exit(2);
}
while(1)
{
string message;
cout << "Enter #";
getline(cin, message);
write(sockfd, message.c_str(), message.size());
char echo_buffer[1024];
n = read(sockfd, echo_buffer, sizeof echo_buffer);
if(n > 0)
{
echo_buffer[n] = 0;
cout << echo_buffer << endl;
}
else
{
break;
}
}