赞
踩
我们进行上网都需要打开软件(上网入口),比如游览器,当打开软件之时,将硬盘上的文件加载到内存之中,在客户端启动了客户端进程。而在服务器上也有对应的服务器进程。然后客户端进程通过网络寻找对应的服务器进程,进行数据的交互。其本质就是进程之间的通信。
在这之中,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 <sys/types.h> #include <sys/socket.h> 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 <sys/socket.h>
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 <sys/types.h> #include <sys/socket.h> 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 <sys/types.h>
#include <sys/socket.h>
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 <iostream> #include<string> #include<unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include<netinet/in.h> #include <sstream> #include<stdlib.h> 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 <iostream> #include<string> #include<unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include<netinet/in.h> #include <stdlib.h> 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; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。