我们进行上网都需要打开软件(上网入口),比如游览器,当打开软件之时,将硬盘上的文件加载到内存之中,在客户端启动了客户端进程。而在服务器上也有对应的服务器进程。然后客户端进程通过网络寻找对应的服务器进程,进行数据的交互。其本质就是进程之间的通信。
在这之中,ip标识全公网内唯一一台主机、端口号port表示该主机中唯一的网络进程、,所以ip+port就是网络之中唯一的进程,即形成了进程之间的通信。
其中ip+port就是套接字,也就是说,套接字本质就是进程间通信。
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号,就是在描述 “数据是谁发的, 要发给谁”。
特点:面向连接,可靠传输,面向字节流
面向连接:TCP通信双方在发送数据之前,需先建立起连接,才能够发送数据。
可靠传输:TCP保证传输的数据是可靠有序的到达对端。
面向字节流:
TCP是提供可靠的传输层协议,因此需要处理的事情就会更多,比如数据会不会丢失,丢失了怎么办等等,因此TCP协议就会更加的复杂,复杂的东西效率也会更低。
特点:无连接,不可靠,面向数据报
无连接:UDP通信双方在发送数据之前,是不需要进行沟通的,客户端只需要知道服务端的ip和端口,就可以直接发送数据了。
不可靠:不能保证数据能到达目的地,并且不保证数据是按序到达(比如先发1在发2,结果是2先到的)。
面向数据报:UDP对于传输层和应用层数据交递的时候,都是整条数据交付。
DUP只负责数据传输,不保证数据是安全达到的,因此UDP协议比较简单,效率也更高。
有些系统的本机字节序是小端字节序, 有些则是大端字节序, 为了保证传送顺序的一致性, 所以网际协议使用大端字节序来传送数据。
这意味着如果是小端机器在传输数据的时候,需要将数据转化为大端字节序进行传输,对端机器默认传输过来的数据是大端字节序。
2个字节 uint16_t htons(uint16_t hostshort)
。
4个字节 uint32_t htonl(uint32_t hostlong)
。
2个字节 uint16_t ntohs(uint16_t netshort)
;
4个字节 uint32_t ntohl(uint32_t netlong)
;
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
操作系统设计sockaddr结构是为了实现一套接口就能完成不同套接字之间的通信(在接口传参时都传入通用结构)。通信前、要将自己的ip地址和端口号发送给对方,因此需要定义一个结构体来保存自己的ip地址和端口号。
在填入参数时,除了需要将主机字节序转化为网络字节序以外,还需要把ip地址从字符串类型转化为四字节的网络字节序(大端)的ip地址。
为什么在数据结构 struct sockaddr_in 中, sin_addr 和 sin_port 需要转换为网络字节顺序,而sin_family 需不需要呢?
sin_addr 和 sin_port 分别封装在包的 IP 和 UDP 层。因此,它们必须要是网络字节顺序。但是 sin_family 域只是被内核 (kernel) 使用来决定在数据结构中包含什么类型的地址,所以它必须是本机字节顺序。同时,sin_family 没有发送到网络上,它们可以是本机字节顺序。
#include
#include
int socket(int domain, int type, int protocol);
domain:地址域
该参数指定网络层使用什么协议
AF_INET:使用ipv4版本的ip协议
AF_INET6:使用ipv6版本的ip协议
AF_UNIX :本地域套接字(适用与一台机器两个进程,进行进程间通信)
type:套接字类型
SOCK_STREAM:流式套接字
SOCK_DGRAM:用户数据报套接字
protocol:使用的协议协议
0:采用套接字类型对应的默认协议
SOCK_DGRAM :默认的协议就是UDP
SOCK_STREAM:默认的协议就是TCP
返回值:
返回一个套接字句柄,本质上是一个文件描述符
创建失败返回-1,成功返回值大于等于0
因为linux之中一切皆文件,要实现网络通信就需要打开网卡设备,所以创建struct file
文件对象,帮我们指向网络信息。
因此socket的返回值是一个文件描述符。
创建网络通信相关的数据结构,这些结构在Linux之中统一被当做文件看待,进程打开文件的方式就是通过文件描述符。类似于open函数。
创建完套接字之后,对应的文件之中只有文件信息,而我们创建的是网络文件,因此需要填入ip、port,将文件信息和网络信息关联起来。
ip和端口号标识网络之中的唯一进程,可以让客户端找到自己。
有了文件信息和网络信息,但是他们之间没有关系,绑定就是让他们之间产生关系。
#include
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
sockfd:socket函数返回的套接字描述符;将创建出来的套接字和网卡,端口号进行绑定
address:给套接字绑定的ip地址和端口号,
address的类型要被强转为通用结构类型struct sockaddr*
address_len:address的长度
返回值:绑定成功返回0,失败返回-1
===
由于UDP并不是面向连接的,所以只需要服务器启动,就可以直接收发消息。
#include
#include
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
sockfd:socket返回的文件描述符
buf:读数据缓冲区
len:期望读取的数据长度
flags:读数据是IO、不一定有数据让你读,如果读取条件不成立,就挂起等待(默认为0,阻塞等待)
src_addr:用来获取发送方的sockaddr,也就是ip地址和端口号数据,如果不关心可以设置为空
addrlen:是一个整数,是实际读到结构体src_addr的大小,这个参数必须要进行初始化
返回值:
实际收到多少个字节的数据,如果为-1则接收错误
size_t无符号整形
ssize_t有符号整形
#include
#include
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,阻塞等待)
src_addr:发送方的sockaddr,也就是ip地址和端口号数据
addrlen:dest_addr结构体的大小
udpServer.hpp:
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
class udpServer{
private:
//std::string ip;
int port;//端口号
int sock;//套接字
public:
//127.0.0.1本地环回,8080默认端口号
//通常用来进行网络代码的本地测试
udpServer(int _port=8080)
:port(_port)
{}
void initServer()
{
//socket的返回值为文件描述符,类似于open函数
sock=socket(AF_INET,SOCK_DGRAM,0);
std::cout<<"sock:"<<sock<<std::endl;
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(port);
//转换ip
//local.sin_addr.s_addr=inet_addr(ip.c_str());
//INADDR_ANY转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP
local.sin_addr.s_addr=INADDR_ANY;
//绑定套接字,ip地址和端口号
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
std::cerr<<"bind error!\n"<<std::endl;
exit(1);
}
}
void start()
{
char msg[64];
//死循环,服务器不退出
for(;;)
{
msg[0]='\0';
struct sockaddr_in end_point;
socklen_t len=sizeof(end_point);
ssize_t s=recvfrom(sock,msg,sizeof(msg)-1,0,(struct sockaddr*)&end_point,&len);
if(s>0)
{
//拿到发送端的ip地址,将整形四字节转成点分十进制的字符串
std::string cli=inet_ntoa(end_point.sin_addr);
cli+=":";
//拿到发送端的端口号
//要将网络字节序转为主机字节序,并且再转成字符串
cli+=std::to_string(ntohs(end_point.sin_port));
msg[s]='\0';
std::cout<<cli<<"#"<<msg<<std::endl;
//再给客户端发消息
std:: string echo_string=msg;
echo_string+="[server echo!]";
sendto(sock,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&end_point,len);
}
}
}
~udpServer()
{
close(sock);
}
};
udpServer.cpp:
#include "udpServer.hpp"
void Usage(std::string proc)
{
std::cout<<"Usage:"<<proc<<"local_port"<<std::endl;
}
int main(int argc,char* argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(1);
}
//绑定端口号
udpServer *up=new udpServer(atoi(argv[1]));
up->initServer();//将服务器初始化
up->start();
delete up;
return 0;
}
udpClient.hpp:
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
class udpClient{
private:
std::string ip;
int port;//端口号
int sock;
public:
//连服务器的ip和端口号
udpClient(std::string _ip="127.0.0.1",int _port=8080)
:ip(_ip)
,port(_port)
{}
void initClient()
{
//创建socket,客户端不需要绑定
sock=socket(AF_INET,SOCK_DGRAM,0);
std::cout<<sock<<std::endl;
}
void start()
{
std::string msg;
//发送给谁
struct sockaddr_in peer;
peer.sin_family=AF_INET;
peer.sin_port=htons(port);
peer.sin_addr.s_addr=inet_addr(ip.c_str());
for(;;)
{
std::cout<<"Please Enter#";
std::cin>>msg;
if(msg=="quit")
{
break;
}
sendto(sock,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));
char echo[128];
//接收服务端发回来的消息
ssize_t s=recvfrom(sock,echo,sizeof(echo)-1,0,NULL,NULL);
if(s>0)
{
echo[s]=0;
std::cout<<"server#"<<echo<<std::endl;
}
}
}
~udpClient()
{
close(sock);
}
};
udpClient.cpp:
#include "udpClient.hpp"
void Usage(std::string proc)
{
std::cout<<"Usage:"<<proc<<"server_ip server_port"<<std::endl;
}
//通过参数列表获取输入的ip和端口号,该ip和端口号就是服务器的ip和端口号
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(1);
}
//绑定ip地址和端口号,端口号是一个整数,要进行强转
udpClient uc(argv[1],atoi(argv[2]));
uc.initClient();
uc.start();
return 0;
}