目录
预备知识
理解源IP地址和目的IP地址 :
认识端口号:
理解"端口号"和"进程ID"
认识TCP/UDP协议
TCP:
UDP :
网络字节序
Socket编程接口
Socket常见API:
Sockaddr结构:
简单的UDP网络程序
实现一个简单的收发功能:
封装一下UdpSocket :
server端:
client端:
makefile:
编辑
地址转换函数
关于inet_ntoa
简单的TCP网络程序
TCP socket API 详解:
socket():
bind():
listen():
accept():
connet():
实现一个简单的收发功能:
封装一下TcpSocket:
server端:
client端:
makefile:
多进程TCP服务器
多线程TCP服务器
TCP协议通讯流程
预备知识
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址。
进程ID也是标识主机上的唯一进程,而端口号也是如此,那为什么端口号不直接使用进程ID呢?
安全性和隔离性:使用端口号可以提供更好的安全性和隔离性。操作系统可以通过控制端口的访问权限和网络流量,确保只有经过授权的应用程序能够监听和使用特定的端口。这样可以减少潜在的安全威胁和攻击。
更加详细的预备知识:
Linux网络基础https://mp.csdn.net/mp_blog/creation/editor/132146472
认识TCP/UDP协议
网络字节序
Q:内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
A:
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
Socket编程接口
//创建 socket 文件描述符(TCP/UDP,客户端 + 服务器)
int socket (int domain, int type,int protocol);
//绑定端口号(TCP/UDP,服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
//开始监听socket(TCP,服务器)
int listen(int socket,int backlog);
//接收请求(TCP,服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
//建立连接(TCP,服务端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
socket API是一层抽象的网络编程接口,适用于各种底层网络ipv4、ipv6以及UNIX Domain Socket
然而各种网络协议的地址格式并不相同.
简单的UDP网络程序
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
class UdpSocket
{
public:
UdpSocket():fd_(-1)
{}
bool Socket()//创建Socket,这一步也可以放构造函数里面
{
fd_ = socket(AF_INET,SOCK_DGRAM,0);
if(fd_<0)
{
perror("socket");
return false;
}
return true;
}
bool Bind(const std::string& ip,uint16_t port)//将IP和端口绑定起来
{
struct sockaddr_in addr;//ipv4所以用_in
addr.sin_family = AF_INET;//表示一下当前的地址族,这里表示ipv4
addr.sin_addr.s_addr = inet_addr(ip.c_str());//对当前的对象addr绑定IP
addr.sin_port = htons(port);//注意大小端所以在这里转化
int isBind = bind(fd_,(sockaddr*)&addr,sizeof(addr));
if(!isBind) {
perror("bind");
return false;
}
return true;
}
bool RecvFrom(std::string* buf,std::string* ip = NULL,uint16_t* port=NULL)
{
char tmp[1024]={0};//一个接收缓冲区
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t read_size = recvfrom(fd_,tmp,sizeof(tmp)-1,0,(sockaddr*)&peer,&len);
if(read_size < 0)
{
perror("recvfrom");
return false;
}
buf->assign(tmp,read_size);//将读到的缓冲区放到输入参数中
if(ip != NULL)
{
*ip = inet_ntoa(peer.sin_addr);
}
if(port != NULL)
{
*port = ntohs(peer.sin_port);
}
return true;
}
bool SendTo(const std::string& buf,const std::string& ip,uint16_t port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip.c_str());
addr.sin_port = htons(port);
ssize_t write_size = sendto(fd_,buf.data(),buf.size(),0,(sockaddr*)&addr,sizeof(addr));
if(write_size < 0)
{
perror("sendto");
return false;
}
return true;
}
bool Close()//要记得关闭
{
close(fd_);
return true;
}
private:
int fd_;
};
//"udp_server.hpp"
#include"udp_socket.hpp"
#include
#include
#include
#include
typedef std::function Handler;
class UdpServer
{
public:
UdpServer(){
assert(sock_.Socket());
}
~UdpServer(){
sock_.Close();
}
bool Start(const std::string& ip,uint16_t port,Handler Handler)
{
//1.创建 socket
//2.绑定 端口号
bool ret = sock_.Bind(ip,port);
if(!ret) return false;
//服务器开启后是一个死循环
while(true)
{
std::cout<<"服务器启动"<
//"udp_server.cc"
#include"udp_server.hpp"
#include
#include
int main(int argc,char* argv[])
{
if(argc!=3){
printf("Usage ./udp_server [ip] [port]\n");
return 1;
}
UdpServer server;
while(1) server.Start(argv[1],atoi(argv[2]),MyTestHandler);
return 0;
}
//"udp_client.hpp"
#include "udp_socket.hpp"
#include
#include
#include
class UdpClient
{
public:
UdpClient(const std::string &ip, uint16_t port) : ip_(ip), port_(port)
{
assert(sock_.Socket());
}
~UdpClient()
{
sock_.Close();
}
bool RecvFrom(std::string *buf)
{
return sock_.RecvFrom(buf);
}
bool SendTo(const std::string &buf)
{
return sock_.SendTo(buf, ip_, port_);
}
private:
UdpSocket sock_;
// 服务器端的 IP 和 端口号
std::string ip_;
uint16_t port_;
};
//"udp_client.cc"
#include"udp_client.hpp"
int main(int argc,char* argv[])
{
if(argc!=3){
printf("Usage ./udp_client [ip] [port]\n");
return 1;
}
//UdpClient client();
std::unique_ptr client(new UdpClient(argv[1],atoi(argv[2])));
for(;;)
{
std::string input;
std::cout<<"来随便输入一点东西:>";
std::cin>> input;
if(!std::cin){
std::cout<<"seeya"<SendTo(input);
std::string result;
client->RecvFrom(&result);
std::cout<<"服务端给了你这么个玩意:>"<< result <
.PHONY:all
all:server client
server:udp_server.cc
g++ -o $@ $^ -std=c++11
client:udp_client.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f server client
地址转换函数
本章值接收基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr.sin_addr表示32位的IP地址,但是我们通常使用的是点分十进制字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;
字符串转in_addr的函数:
#include
int inet_aton(const char* strptr,struct in_addr* addrptr);
in_addr_t inet_addr(const char* strptr);
int inet_pton(int family,const char* strptr,void* addrptr);
in_addr转字符串的函数:
char* inet_ntoa(struct in_addr inaddr);
const char* inet_ntop(int family,const void *addrptr,char* strptr,size_t len);
其中inet_pton 和int_ntop不仅可以转化IPv4的in_addr,还可以转化IPv6的in6_addr,因此接口函数是void* addrptr;
示例:
#include
#include
#include
#include
int main()
{
struct sockaddr_in addr;
inet_aton("127.0.0.1", &addr.sin_addr);
uint32_t* ptr = (uint32_t*)(&addr.sin_addr);
printf("addr:%x\n",*ptr);
printf("addr_str:%s\n",inet_ntoa(addr.sin_addr));
return 0;
}
//输出结果
//addr:100007f
//addr_str:127.0.0.1
inet_ntoa这个函数返回了一个char* ,很显然是这个函数在机子内部为我们申请了一块空间来保存ip的结果,那么是否需要调用者手动释放呢?
man手册上说,inet_ntoa函数,是把这个返回值放到了静态存储区,这时不需要手动进行释放
但如果我多次调用会发生什么?
#include
#include
#include
#include
int main()
{
struct sockaddr_in addr1;
struct sockaddr_in addr2;
addr1.sin_addr.s_addr = 0;
addr2.sin_addr.s_addr = 0xffffffff;
char* ptr1 = inet_ntoa(addr1.sin_addr);
char* ptr2 = inet_ntoa(addr2.sin_addr);
printf("ptr1:%s,ptr2:%s\n",ptr1,ptr2);
return 0;
}
//输出结果
//ptr1:255.255.255.255, ptr2:255.255.255.255
因为inet_ntoa把结果放到自己内部 的一个静态存储区,这样二次调用就会覆盖上一次的结果
简单的TCP网络程序
下面所用的socket API这些函数都在sys/socket.h中
#include
#include
int socket(int domain,int type,int protocol);
#include
#include
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
程序中可以这么对myaddr的参数进行初始化
bzero(&servadddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htol(INADDR_ANY);
servaddr.sin_port = htos(SERV_PORT);
#include
#include
int listen(int sockfd,int backlog);
#include
#include
int accept(int sockfd,struct sockaddr *addr,socklen_t* addrlen);
accept的返回值:
常见的accept的错误码:(通过errno全局变量访问)。常见的错误码包括EAGAIN(指示套接字非阻塞且没有等待连接)、EINTR指示调用被信号中断)、EINVAL(指示套接字无效)等。
#include
#include
int connet(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
//"tcp_socket.hpp"
#pragma
#include
#include
#include
#include
#include
#include
#include
#include
class TcpSocket
{
public:
TcpSocket():fd_(-1){}
TcpSocket(int fd):fd_(fd){}
bool Socket()
{
fd_ = socket(AF_INET,SOCK_STREAM,0);//注意这里是SOCK_STREAM跟udp不一样
if(fd_<0){
perror("socket");
return false;
}
printf("opened fd = %d\n",fd_);
return true;
}
bool Close(){
printf("close fd = %d",fd_);
close(fd_);
return true;
}
bool Bind(const std::string& ip,uint16_t port){
struct sockaddr_in addr;
addr.sin_family = AF_INET;//绑定协议族一样是AF_INET(ipv4)
addr.sin_addr.s_addr = inet_addr(ip.c_str());//这里IP也可以绑定成INADDR_ANY
addr.sin_port = htons(port);
int ret = bind(fd_,(struct sockaddr*)&addr,sizeof(addr));
if(ret<0){
perror("bind");
return false;
}
return true;
}
bool Listen(int num){
int ret = listen(fd_,num);
if(ret<0){
perror("listen");
return false;
}
return true;
}
bool Accept(TcpSocket* peer,std::string* ip,uint16_t* port = NULL)
{
struct 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){
buf->clear();
char tmp[1024]={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){
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)
{
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_,(struct sockaddr*)&addr,sizeof(addr));
if(ret<0){
perror("connect");
return false;
}
return true;
}
public:
int fd_;
};
//"tcp_server.hpp"
#include"tcp_socket.hpp"
typedef std::function Handler;
class TcpServer
{
public:
TcpServer(){}
TcpServer(std::string ip,uint64_t port)
:ip_(ip),port_(port)
{}
bool Start(Handler handler)
{
//创建socket
listen_sock_.Socket();
//绑定端口号
listen_sock_.Bind(ip_,port_);
//进行监听
listen_sock_.Listen(10);
//进入事件
for(;;)
{
TcpSocket new_sock;//客户端
std::string ip;
uint16_t port = 0;
if(!listen_sock_.Accept(&new_sock,&ip,&port))//在我accept成功后获取底层链接就成功了,之后用新的new_sock和client的ip、端口进行通信
{
continue;//这里的ip和port都改成client端的,而new_sock也是连接成功后新生成的
}
//所以这时候new_sock里面的_fd就是新的fd,使用new_sock就可以进行通信
printf("[client %s:%d] connect!\n",ip.c_str(),port);
while(true)
{
std::string req;
bool ret = new_sock.Recv(&req);
if(!ret){
printf("[client %s:%d] disconnect!\n",ip.c_str(),port);
new_sock.Close();
break;
}
//处理接收的req
std::string resp;
handler(req,&resp);
new_sock.Send(resp);
printf("[%s:%d]req:%s,resp:%s\n",ip.c_str(),port,req.c_str(),resp.c_str());
}
}
return true;
}
private:
TcpSocket listen_sock_;
std::string ip_;
uint16_t port_;
};
//"tcp_server"
#include"tcp_server.hpp"
#include
void MyTestHandler(const std::string& req,std::string* resp)
{
//这里实现服务器方法
*resp = req;
}
int main(int argc,char* argv[])
{
if(argc!=3){
exit(-1);
}
std::unique_ptr ser(new TcpServer(argv[1],atoi(argv[2])));
ser->Start(MyTestHandler);
return 0;
}
//"tcp_client.hpp"
#include"tcp_socket.hpp"
class TcpClient
{
public:
TcpClient(const std::string& ip,uint16_t port)
:ip_(ip)
,port_(port)
{
sock_.Socket();
}
~TcpClient()
{
sock_.Close();
}
bool Connect()
{
return sock_.Connect(ip_,port_);
}
bool Recv(std::string* buf)
{
return sock_.Recv(buf);
}
bool Send(const std::string& buf)
{
return sock_.Send(buf);
}
private:
TcpSocket sock_;
std::string ip_;
uint16_t port_;
};
//"tcp_client.cc"
#include"tcp_client.hpp"
#include
#include
int main(int argc,char* argv[])
{
std::unique_ptr client(new TcpClient(argv[1],atoi(argv[2])));
bool ret = client->Connect();
if(!ret) return 1;
for(;;)
{
std::cout<<"输入向服务器发出的数据:>";
std::string input;
std::cin >> input;
if(!std::cin){
break;
}
client->Send(input);
std::string res;
client->Recv(&res);
std::cout<< res << std::endl;
}
return 0;
}
.PHONY:all
all:server client
server:tcp_server.cc
g++ -o $@ $^ -std=c++11
client:tcp_client.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f server client
注意:
多进程TCP服务器
上面所写的程序只能单个客户端连接,这显然是不符合常理的,所以我们可以用多进程的方法:
TcpProcessServer:
//"TcpProcessServer"
#pragma once
#include
#include
#include "tcp_socket.hpp"
typedef std::function Handler;
// 多进程版本的 Tcp 服务器
class TcpProcessServer
{
public:
TcpProcessServer(const std::string &ip, uint16_t port) : ip_(ip), port_(port)
{
// 需要处理子进程
signal(SIGCHLD, SIG_IGN);
}
void ProcessConnect(TcpSocket &new_sock, const std::string &ip, uint16_t port, Handler handler)
{
int ret = fork();
if (ret > 0)
{
// father
// 父进程不需要做额外的操作, 直接返回即可.
// 思考, 这里能否使用 wait 进行进程等待?
// 如果使用 wait , 会导致父进程不能快速再次调用到 accept, 仍然没法处理多个请求
// [注意!!] 父进程需要关闭 new_sock
new_sock.Close();
return;
}
else if (ret == 0)
{
// child
// 处理具体的连接过程. 每个连接一个子进程
for (;;)
{
std::string req;
bool ret = new_sock.Recv(&req);
if (!ret)
{
// 当前的请求处理完了, 可以退出子进程了. 注意, socket 的关闭在析构函数中就完成了
printf("[client %s:%d] disconnected!\n", ip.c_str(), port);
exit(0);
}
std::string resp;
handler(req, &resp);
new_sock.Send(resp);
printf("[client %s:%d] req: %s, resp: %s\n", ip.c_str(), port,
req.c_str(), resp.c_str());
}
}
else
{
perror("fork");
}
}
bool Start(Handler handler)
{
// 1. 创建 socket;
listen_sock_.Socket();
// 2. 绑定端口号
listen_sock_.Bind(ip_, port_);
// 3. 进行监听
listen_sock_.Listen(5);
// 4. 进入事件循环
for (;;)
{
// 5. 进行 accept
TcpSocket new_sock;
std::string ip;
uint16_t port = 0;
if (!listen_sock_.Accept(&new_sock, &ip, &port))
{
continue;
}
printf("[client %s:%d] connect!\n", ip.c_str(), port);
ProcessConnect(new_sock, ip, port, handler);
}
return true;
}
private:
TcpSocket listen_sock_;
std::string ip_;
uint64_t port_;
};
多线程TCP服务器
TcpPhtreadServer.hpp
//"TcpPthreadServer.hpp"
#pragma once
#include
#include
#include "tcp_socket.hpp"
typedef std::function Handler;
struct ThreadArg
{
TcpSocket new_sock;
std::string ip;
uint16_t port;
Handler handler;
};
class TcpThreadServer
{
public:
TcpThreadServer(const std::string &ip, uint16_t port) : ip_(ip), port_(port)
{
}
bool Start(Handler handler)
{
// 1. 创建 socket;
listen_sock_.Socket();
// 2. 绑定端口号
listen_sock_.Bind(ip_, port_);
// 3. 进行监听
listen_sock_.Listen(5);
// 4. 进入循环
for (;;)
{
// 5. 进行 accept
ThreadArg *arg = new ThreadArg();
arg->handler = handler;
bool ret = listen_sock_.Accept(&arg->new_sock, &arg->ip, &arg->port);
if (!ret)
{
continue;
}
printf("[client %s:%d] connect\n", arg->ip.c_str(), arg->port);
// 6. 创建新的线程完成具体操作
pthread_t tid;
pthread_create(&tid, NULL, ThreadEntry, arg);
pthread_detach(tid);
}
return true;
}
// 这里的成员函数为啥非得是 static?
static void *ThreadEntry(void *arg)
{
// C++ 的四种类型转换都是什么?
ThreadArg *p = reinterpret_cast(arg);
ProcessConnect(p);
// 一定要记得释放内存!!! 也要记得关闭文件描述符
p->new_sock.Close();
delete p;
return NULL;
}
// 处理单次连接. 这个函数也得是 static
static void ProcessConnect(ThreadArg *arg)
{
// 1. 循环进行读写
for (;;)
{
std::string req;
// 2. 读取请求
bool ret = arg->new_sock.Recv(&req);
if (!ret)
{
printf("[client %s:%d] disconnected!\n", arg->ip.c_str(), arg->port);
break;
}
std::string resp;
// 3. 根据请求计算响应
arg->handler(req, &resp);
// 4. 发送响应
arg->new_sock.Send(resp);
printf("[client %s:%d] req: %s, resp: %s\n", arg->ip.c_str(),
arg->port, req.c_str(), resp.c_str());
}
}
private:
TcpSocket listen_sock_;
std::string ip_;
uint16_t port_;
};
TCP协议通讯流程
下面是基于TCP协议的客户端/服务器程序的一般流程:
服务器初始化:
- 调用socket,创建文件描述符
- 调用bind,将当前的文件描述符和ip/port绑定在一起,如果端口被占用的话就会绑定失败
- 调用listen,声明当前这个文件描述符作为一个服务器的文件描述符,为后面的accept做好准备
- accept并阻塞等待客户端连接过来
建立连接的过程:
- 调用socket,创建文件描述符
- 调用connect,向服务器发起连接请求
- connect会发出SYN段并阻塞等待服务器应答;(第一次)
- 服务器收到客户端的SYN,会应答一个SYN-ACK表示“同意建立连接”(第二次)
- 客户端收到SYN-ACK后会从connect()返回,同时应答一个ACK段(第三次)
这个建立连接的过程,通常称为三次握手
数据传输的过程:
- 建立连接够,TCP协议提供全双工的通信服务,所谓全双工的意思就是,在同一条连接中,同一时刻,通信双方可以同时写数据,相对的概念叫做半双工,同一条连接在同一时刻,只能由一方来书写数据
- 服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待
- 这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端抵用read()阻塞等待服务器的应答
- 服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求
- 客户端收到后从read()返回,发送下一条请求,如此循环
断开连接的过程:
- 如果客户端没有更多的请求,就调用close()关闭连接,客户端会向服务器发送FIN段(第一次)
- 此时服务器收到FIN后,会会应一个ACK,同时read也会返回0(第二次)
- read返回之后,服务器就知道客户端关闭了连接,也调用close关闭,这时候服务器会响客户端发送一个FIN(第三次)
- 客户端收到FIN,再返回一个ACK给服务器(第四次)
这个断开的过程中通常称为四次挥手