目录
一. 套接字基本概念
IP地址
TCP和UDP协议
端口号
端口号vs 进程pid
网络字节序
本地字节序转换成网络字节序
网络字节序转换为本地字节序
二. 套接字的基本操作
socket的创建
域(domain)
类型(type)
协议(Protcol)
返回值
struct socketaddr地址结构
struct sockaddr 结构
struct sockaddr_in 结构
socket绑定地址(bind函数)
Socket监听连接(listen函数)
Socket请求连接(connect函数)
Socket接受连接(accept函数)
Socket接受数据 (recvfrom函数)
Socket发送数据(sendto函数)
Socket关闭
三. UDP编程模型
四. Tcp编程模型
多进程服务器
多线程服务器
线程池服务器
五. netstate命令
所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象,一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制
IP地址是在IP协议中,主要功能是用来标识网络上不同主机的地址。
IP地址是由32位组成,主要由三部分:地址类别,网络号和主机号:
TCP/IP协议传输层使用最广泛的两个协议分别是TCP协议和UDP协议,UDP套接口是数据报套接字的一种,而TCP套接口是字节流套接字的一种。
TCP/IP协议传输层的 主要任务是向位于不同的(有时候位于同一主机)上的应用程序提供端到端的通信服务,为了区分应用程序,TCP和UDP引入了端口号的概念,端口本质是一个16位的整数
端口号(port)是传输层协议的内容
任何的网络服务与网络客户端,如果要进行正常的数据通信,必须使用端口号,来唯一标识进程。
公网ip:唯一的标识全网唯一的一台主机
socket通信,本质是进程间通信,跨网络的进程进通信。
在linux操作系统内,进程pid也是唯一标识进程,端口号跟进程pid之间有什么区别?举个例子:
在国家的社会中,每个人都有一个身份证号用来唯一标识一个人,在大学中,每个学生都有一个学生号来唯一标识一个学生,为什么学校不用身份证号来唯一区分一个学生呢?因为使用身份证号的人数多,有14亿左右,并不是每个人都在这个大学中读书,而一个大学中只有几千个人,那么为了能够方便管理和查找,学校再使用一个学号来唯一标识学校中学生的身份。
而进程pid就好比进程的身份证号,而端口号就好比学号,在一台机器上,假设有50个进程在运行,但是只有5个进程进行网络通信,这时候我们只要给这5个进程分配端口号即可。
由于不同计算机系统采用不同的字节序存储数据,同样一个4字节的32位整数在内存中存储的方式是不同的,这称为本地字节序。字节序列分为大端字节序和小端字节序,Intel处理器大多使用大端字节序,Motoro大多使用小端字节序。小端字节序是指低位字节存放内存的低地址处,大端字节序指的是高位字节存储在内存的低地址处。
如果进程只在单机环境下运行,并且不和其他进程打交道,我们完全可以忽略字节序的存在,但是,如果进程需要跟其他计算机上的进程进行交换,我们必须考虑字节序的问题。
1的字节序分别在大端机器和小端机器存储情况,如果大端机器将内存中的数据通过网络传给小端机器,那么小端机器从网络获得的数据放进内存中的数据是相反的。这就是网络字节序问题。
TCP/IP协议进行网络数据传输时,规定一种数据表示格式,它与具体的cpu和操作系统无关,保证了数据在不同主机传输时能够被正确解释,这就是网络字节序,网络字节序统一采用大端字节序。
因此,当两台机器进行通信时,必须先将本地字节序转换为网络字节序转化为网络字节序在进行发送,当收到数据之后,应当将网络字节序转化为本地字节序再进行后续使用。
#include
//将32位的整型数据从本地转换为网络字节序
uint32_t htonl(uint32_t hostlong);
//将16位的整形数据从本地转换为网络字节序
uint16_t htons(uint16_t hostshort);
#include
//将32位的整形网络序列转换为本地序列
uint32_t ntohl(uint32_t netlong);
//将16位的整形网络序列转换为本地序列
uint16_t ntohs(uint16_t netshort);
#include /* See NOTES */
#include
int socket(int domain, int type, int protocol);
域指定了Socket编成模型的地址族,其类型为int,常见的取值如下表所示:
网络编程中最常用的socket域取值是AF_INET和AF_INET6协议,其中AF_INET6协议是下一代互联网的协议,客服目前IPv4存在可用地址有限的问题,但AF_INET6协议还没有被实际运用,在写代码时一般使用AF_INET.
类型主要指定了通信双方的数据传输格式,比较常见的数据格式类型有三种:
协议主要指通信双方的约定,如TCP协议和UDP协议,但正常情况下,当双方的域(domain)和通信数据类型(type)确定以后,协议就会被唯一确定l,传0表示根据domain和type类型推出协议。所以该参数一般传0即可。
函数socket()的返回值是一个文件描述符。套接字的建立本质是一个进程在内存中打开一个文件,并且这个文件中的数据与网卡互相连接,然后将打开的文件描述符返回给进程,然后进程可以对这个文件可以进行读写数据。
成功返回一个文件描述符(socket描述符),失败返回-1,同时errno被设置成相应的值。
创建socket本质是一个进程在内存中打开一个文件缓冲区,然后这个文件缓冲区与网卡进行数据交互。
在各种底层网络协议中,如IPv4和IPv6,以及UNIX DOMAIN socket,这些底层网络协议地址格式不同,所以为了兼容这些底层网络协议,Socket API定义了一个通用的struct socketaddr,这使得不同的地址结构可以被bind(),connect,revfrom,sendto()等函数调用。
struct sockaddr {
sa_family_t sa_family; //地址族 16位
char sa_data[14]; //14个字节,包括目标地址和端口号
}
struct sockaddr_in {
__kernel_sa_family_t sin_family; // 地址族 16位
__be16 sin_port; //端口号,16位,可以看成一个int类型
struct in_addr sin_addr; //ip地址 32位
//填充信息,一般不需要管
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
//以整数的形式指定套接字的网络地址
struct in_addr {
__be32 s_addr; //存放32位ip地址
};
IPv4地址使用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位ip地址。
若要使socket也可以被其他进程使用,服务器必须给socket绑定ip地址和端口号。
#include /* See NOTES */
#include
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数sockfd为待绑定的socket描述符,参数addr是本地sockaddr地址结构指针,addrlen为本地sockaddr地址结构的大小。执行成功返回0,失败返回-1.`
对于绑定IP4套接字,我们不会直接去创建一个struct sockaddr变量,而是直接定义一个struct sockaddr_in变量,然后将ip地址和端口号填充进struct sockaddr_in变量中,最后在传参的时候将struct sockaddr_in*变量强制转换为struct sockaddr*类型,其他套接字也是一样。
绑定IP4套接字过程如下:
struct sockaddr_in s; //创建IP4网套接字地址数据结构
memset(&s,'\0',sizeof(s)); //将s置为0
s.sin_family=AF_INET; //设置TCP/IP地址族
s.sin_port=htons(port); //设置端口号
s.sin_addr.s_addr=0; //系统自动填入本机IP地址
bind(fd,(struct sockaddr*)&s,sizeof(s) //绑定套接字
//将字符型IP转换为32位整数型ip
in_addr_t inet_addr(const char *cp);
//将网络地址转换为字符串
char *inet_ntoa(struct in_addr in)
IP地址也可以不用直接填充,使用INADDR_ANY变量,可以直接填入本机IP地址,推荐使用这种方法。在我们的云服务器中,一般使用这种操作填充IP地址,因为云服务器的IP是由云厂商提供的,这个云服务器IP不能直接被绑定。
my_addr.sin_addr.s_addr=INADDR_ANY; //填入本机IP地址
一般只有服务器才会去绑定IP地址和端口号 ,客户端不需要绑定IP地址和端口号。因为服务器需要不断的监听客户端的请求,服务器的IP地址和端口号它不可以一直改变,而客户端在连接服务器或者给服务器发送数据时,系统会给客户端随机分配一个端口,这个端口号和ip地址会发送给服务器,接下来服务器根据ip地址和端口号就可以给客户回应消息。
在成功建立Socket并完成与本地地址绑定后。使用listen()函数来监听客户的连接请求。调用函数listen()函数会创建一个等待队列,在其中存放未处理的客户端请求,其函数原型如下:
#include
#include
int listen(int sockfd, int backlog);
参数为Socket描述符,参数为backlog为请求队列中允许的最大请求数,系统默认为5。函数listen()执行成功返回0,若执行失败返回-1.同时errno被设置成相应的值。
Tcp协议中,函数connect()用于客户端向服务器发起连接请求;UDP协议是面向无连接的,因此无需使用connect(),其函数原型如下:
#include /* See NOTES */
#include
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
sockfd为待绑定Socket的描述符:参数addr是服务器Socket地址结构指针,参数addrlen为addr的大小。
函数执行成功返回0,失败返回-1,同时errno被设置成相应的值。
服务器进程调用函数listen()创建等待队列之后,调用accept()函数等待并接受客户端的连接请求,函数accept通常从连接等待队列中取出一个未处理的连接请求,其函数
#include
#include
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
其中,参数sockfd为Socket的描述符;参数addr为存放客户端Sock地址结构指针,参数addrlen用于存放客户端Socket地址结构指针。
函数accept()执行成功,返回客户端新的套接字描述符;若执行失败,返回-1,同时errno被设置成相应的值。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数sockfd是Socket描述符,buf指定接受数据存放的缓冲区,len用于指定接受数据的大小,参数用于指定接受数据的标志,一般设置为0.
函数recvfrom中,参数src_addr用于存放数据发送方的网络地址结构(ip4是由sockaddr_in填充对象强转为sockaddr类型),参数addrlen是src_addr的大小。
recv和recvfrom执行成功,返回实际接受的字节数,执行失败,返回-1.
函数send()和sendto()向socket连接中发送数据,函数原型如下:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd是Socket描述符,参数buf用于指定发送数据的缓冲区,参数len是指定发送数据的大小,参数flags用于指定发送数据标志,一般设置为0.
函数sendto(),dest_addr用于指定接受方的网络地址结构(ip4是由sockaddr_in填充对象强转为sockaddr类型),addrlen是dest_addr的大小。
函数send()和sendto()发送数据成功,返回实际发送出数据的大小,失败返回-1.
因为socketfd是文件描述符,所以用户也可以用close()函数来终止服务器与客户端的套接字的连接。
#include
int close(int fd);
close函数执行成功,返回0,失败返回-1.
UDP编程是不需要连接的,它是面向数据报。UDP socket是不可靠的,报文可能会丢失,重复或达到的顺序与它发送时的顺序不同;第二,当UDP上的socket填满的数据时,再发送数据时则报文会丢弃。
代码:
server.hpp文件
#include
#include
#include
#include
#include
#include
#include
using namespace std;
class server
{
private:
int port;
string ip;
int fd;
public:
server(string _ip,int _port)
W> :ip(_ip),port(_port)
{}
bool initUdpServer()
{
//创建套接字
fd=socket(AF_INET,SOCK_DGRAM,0);
if(fd<0)
{
cout<<"socket fail\n"<0)
{
buf[sz]='\0';
int _port=ntohs(peer.sin_port);
string s=inet_ntoa(peer.sin_addr);
cout<
server.cc文件
#include"server.hpp"
#include
int main(int argc,char* argv[])
{
if(argc!=2)
{
cout<<"Usage :"<initUdpServer();
s->Start();
return 0;
}
client.hpp文件
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
class client
{
private:
int sockfd;
int sev_port;
string sev_ip;
public:
client(string _sev_ip,int _sev_port)
:sev_port(_sev_port)
,sev_ip(_sev_ip)
{
}
bool InitClient()
{
sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0)
{
cout<<"socket error"<>buf;
sendto(sockfd,(void*)buf.c_str(),(size_t)buf.size(),0,(struct sockaddr*)&dest,sizeof(dest));
}
}
};
client.cc文件
#include"client.hpp"
//./client "127.0.0.1" 8081
int main(int argc,char* argv[])
{
if(argc!=3)
{
cout<<"Usuage"<<"server_ip server_port"<InitClient();
c->start();
return 0;
}
Tcp协议再两个端点(即应用程序)之间提供了可靠的,面向连接的,双向字节流的通信管道。
Tcp协议提供了客户端和服务器之间的连接。Tcp客户端首先给某个定服务器建立一个连接,然后再通过连接与服务器进行数据交换,最后终止这个连接。
Tcp协议保证了数据传输的可靠性,当Tcp一端向另一端发送数据时,它要求另一端返回一个“确认”,如果没有收到“确认”,Tcp就会自动多次传送数据,在数次重传失败后才会放弃。
Tcp协议提供的连接是全双工的,这意味着在一个给定的连接上,一个Tcp端点可以在同一时刻发送数据又接受数据。
Tcp协议
Tcp服务器模型
Tcp服务器和客户端接收和发送数据可以用read和write,首先,前面socket描述符本质是文件描述符,所以可以用文件的操作接口, 其次,服务器在accept()后连接客户端就可以拿到客户端的IP和端口号信息。所以服务器和客户端接收数据和发送数据正常使用read()函数和write()函数。
read()如果大于0,说明读到了信息。如果等于0,说明对端已关闭。
makefile文件
.PHONY:all
all:tcp_client tcp_server
tcp_client:tcp_client.cc
g++ $^ -o $@
tcp_server:tcp_server.cc
g++ $^ -o $@
clean:
rm -f tcp_client tcp_server
服务器头文件
下面是单线程单进程的服务器,一次只能连接一个客户端。
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
class TcpServer
{
private:
int port;//端口号
int listen_sock;//监听套接字描述符
public:
TcpServer(int _port=8081)
:port(_port)
,listen_sock(-1)
{
}
void InitServer()
{
listen_sock=socket(AF_INET,SOCK_STREAM,0);//创建套接字
if(listen_sock<0)
{
std::cerr<<"socket fail"<0)
{
buffer[size]='\0';
std::cout<
服务器.cc文件
#include"tcp_server.hpp"
using namespace std;
//启动服务器的方式:./tcp_server port
int main(int argv,char* argc[])
{
if(argv!=2)
{
cout<<"Usage: tcp_server port"<InitServer();//初始化服务器
ts->Loop();//启动服务器
return 0;
}
客户端头文件
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
class TcpClient
{
private:
int ser_port;//服务器端口号
std::string ser_ip;//服务器的ip地址
int sockfd;//本地套接字描述符
public:
TcpClient(std::string _ser_ip,int _ser_port):
ser_port(_ser_port)
,ser_ip(_ser_ip)
,sockfd(-1)
{
}
void InitTcpClient()
{
sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
if(sockfd<0)
{
std::cerr<<"socket failing"<>message;
//往套接字中输入数据
write(sockfd,message.c_str(),message.size());
ssize_t size=read(sockfd,buffer,sizeof(buffer));
if(size>0)
{
buffer[size]='\0';
std::cout<<"server # "<0)
close(sockfd);
}
};
客户端.cc文件
#include"tcp_client.hpp"
using namespace std;
#include
// 启动客户端的方式:./tcp_client ip port
int main(int argv,char* argc[])
{
if(argv!=3)
{
cout<<"Usage : ./tcp_client ip port"<InitTcpClient();
tc->Start();//开始运行服务端
return 0;
}
运行结果:
因为这是单线程单进程的服务器,所以服务器每次只能接受一个客户端的连接。
多进程服务器只需要在server.hpp文件改Loop()函数,其他文件不用改。
主线程是爷爷进程,爷爷进程不断的去等待队列中接受新连接,接受到一个新连接,就去创建一个爸爸进程,再创建一个儿子进程去服务客户端的连接请求,再退出爸爸进程,如果有多个连接,爷爷进程就会创建多个儿子进程去服务多个连接。为什么要创建儿子进程?如果只创建爸爸进程,那么爸爸进程再退出前,爷爷进程需要去回收爸爸进程的资源,爷爷进程就会阻塞等待,如果再创建一个儿子进程,再退出爸爸进程,那么儿子进程就会被操作系统给“领养",当儿子系统退出时,操作系统会自动释放儿子进程的资源,此时爷爷进程就不会被阻塞,继续去接收新的连接。
当创建儿子进程去服务新链接后,爷爷进程就需要关闭新连接的sock描述符,防止爷爷进程的文件描述符表泄漏。
运行结果
多线程服务器代码
class Pragma
{
public:
int sockfd;
std::string ip;
int port;
public:
Pragma(int _sockfd,std::string _ip,int _port)
:sockfd(_sockfd)
,ip(_ip)
,port(_port)
{
}
};
class TcpServer
{
private:
int port;
int listen_sock;
public:
TcpServer(int _port)
:port(_port)
,listen_sock(-1)
{
}
void InitTcpServer()
{
listen_sock=socket(AF_INET,SOCK_STREAM,0);
if(listen_sock<0)
{
std::cerr<<"socket failing"<sockfd,p->ip,p->port);
close(p->sockfd);
delete p;
W> }
static void Server(int sockfd,std::string ip,int port)
{
char buffer[1024];
while(true)
{
buffer[0]='0';
int sz=read(sockfd,buffer,sizeof(buffer)-1);
if(sz>0){
buffer[sz]='\0';
std::cout<0)
close(listen_sock);
}
};
多线程服务器,当主线程接受连接多少个客户端时,主线程会创建相应的子线程去服务客户端,同时主线程和客户端不能随便关闭socket描述符,因为主线程和子线程使用的同一文件描述符表。
线程池Tcp服务器在服务器内创建一个线程池,初始化线程池后,服务器的主线程不断的往线程池中的任务队列中塞连接请求任务,线程池中的线程不断在任务队列中取出连接请求任务,通过客户端的套接字描述符,线程池中的线程就可以跟客户端进行通信。
线程池Tcp服务器处理客户端的连接请求的线程是固定的,不会随着连接请求的增多而导致线程不断的增多。
线程池的实现
Task.hpp文件
#pragma once
#include
#include
#include
class Handler{
public:
Handler()
{}
void operator()(int sockfd,std::string ip,int port)
{
char buffer[1024];
while(true)
{
buffer[0]='0';
int sz=read(sockfd,buffer,sizeof(buffer)-1);
if(sz>0){
buffer[sz]='\0';
std::cout<
threadpool.hpp头文件(线程池)
#pragma once
#include
#include
#include
#include
using namespace std;
#define NUM 2
template
class ThreadPool
{
private:
queue q;//任务队列
int thread_num;//线程池的线程数量
pthread_mutex_t lock;//互斥锁
pthread_cond_t cond;//条件变量
public:
ThreadPool(int num=NUM)//构造函数
:thread_num(num){
pthread_mutex_init(&lock,NULL);
pthread_cond_init(&cond,NULL);
}
bool Empty()
{
return q.size()==0?true:false;
}
static void* Routine(void* arg)//线程执行流
{
pthread_detach(pthread_self());//线程分离
ThreadPool* self=(ThreadPool*)arg;
while(1)
{
self->LockQueue();
while(self->Empty())//任务队列是否为空
{
self->Wait();
}
T data;
self->Pop(data);//取出任务
self->UnlockQueue();
cout<
tcp_server.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include"threadpool.hpp"
#include"Task.hpp"
class TcpServer
{
private:
int port;
int listen_sock;
ThreadPool* pool;
public:
TcpServer(int _port)
:port(_port)
,listen_sock(-1)
,pool(nullptr)
{
}
void InitTcpServer()
{
listen_sock=socket(AF_INET,SOCK_STREAM,0);//创建监听套接字
if(listen_sock<0)
{
std::cerr<<"socket failing"<();//创建线程池
}
void Loop()
{
pool->ThreadPoolInit();//初始化线程池
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
for(;;)
{
//主线程接受客户端的连接
int sockfd=accept(listen_sock,(struct sockaddr*)&peer,&len);
if(sockfd<0)
{
continue;
}
std::string ip=inet_ntoa(peer.sin_addr);
int port=ntohs(peer.sin_port);
std::cout<<"get new link ["<Push(t);//将任务推到线程池中
}
}
~TcpServer()
{
if(listen_sock>0)
close(listen_sock);
}
};
netstate 命令用于显示各种网络相关信息,如网络连接,路由表,接口状态 (Interface Statistics),masquerade 连接,多播成员 (Multicast Memberships) 等等。