Linux网络套接字
目录
1.温故知新
2.网络套接字介绍
1.温故知新
我们知道我们为了解决长距离传输出现的问题,我们制定了协议,我们还知道我们的数据传输只是手段,我们传输数据的目的是让我们的数据使用起来,说人话就是数据是给人用的。那么如何让我们的人看到数据呢?我们先想一下我们日常生活中使用的迅雷QQ等各种各样的进程,我们要数据本质是让我们的进程使用数据,所以当我们把数据给到我们一个一个具体的进程的时候,数据就会被用起来,你把种子给迅雷,迅雷就会帮你下载对应的数据,所以我们的网络通信本质是不同的进程间进行通信,要对网络通信祛魅,不要认为它是非常困难的东西,我们还知道我们的每一台主机都有IP,那么我把数据传到对应IP的主机接下来怎么办?这就要交给我们的端口了,我们的端口是指导我们数据具体交给哪个进程的一个数值。
这里对于端口号,我们明确给出定义,端口号就是标定一个主机内进行的进程的标志,一个端口号在一台主机内只能被一个进程占用,而我们有些端口被我们的应用层指定占用,比如我们的http,ssh等协议就有指定的端口号,当我们把数据交到应用层,它们就会去这个进程里面处理。
所以我们得出结论:ip+端口就可以锁定全网内一个指定主机上的进程!
IP锁定主机,端口锁定主机上的进程。每一个值都有其对应要完成的工作!通信的时候,就是我们网络的两个进程代表我们的人来进行进程间通信。
然后我们的网络传输层是属于我们操作系统的,我们要访问我们的网络传输层,必须要通过系统调用才能访问,这是因为我们系统中就已经知道的,我们的操作系统并不相信人类。
2.网络套接字介绍
学习C语言的时候,我们知道我们的数据存储有大端存储和小端存储,在网络通信的时候,如果它们的存储方式不一样是无法读取到正确的数据的,比如你认为这个数字是1234,你发过去它就认为是4321,虽然数字一样,但是代表的含义却大相径庭,所以我们在网络通信的时候协议规定,我们必须使用大端通信!
网络通信的时候,我们要创建我们的套接字socket,也就是在我们的进程内创建一个文件,给我们返回它的文件下标,我们就能找到这个文件,然后我们绑定端口号,就是把我们的ip和端口号进行初始化,如果是TCP,我们还需要进行监听,接受请求,客户端要进行建立链接。我们进行这个工作的时候,我们不同的通信协议具有不同的格式。
比如我们的网络通信就需要sockaddr_in,本地通信就需要sockaddr_un,它们的格式不一样,但是为了降低学习成本,我们把它设计成前16位是统一的,如果我们想知道我们的结构体是哪种,我们只需要读前16位,我们就可以知道它是要进行网络通信还是要进行本地通信。这种设计模式和我们C嘎嘎中的多态是一脉相承的,它也是通过不同的对象,一样的函数调不同的结果实现多态的,由于工程中这种设计模式的常见,我们才把它设计成语言的特性,也可以说是行业的特性。
我们的通信有UDP还有TCP,UDP是比较简单的,我们先从UDP切入,我们的UDP不保证数据的安全,只是发送,也不保证顺序,它做的就是把数据发出去了,至于发出去的数据的状况它是不会关心的,所以这就导致了我们UDP做的工作必然简单,我们UDP进行网络通信要进行
1,套接字的创建
2.绑定端口IP
3.发送数据或者接受数据
我们UDP是属于我们操作系统内核的,我们访问它必须要借助我们的系统调用才能进行,所以我们下面依次来说,我们首先创建套接字:
我们套接字的代码如下:第一个参数是我们的选项,如果我们进行网络通信我们一般都要选AF_INET,然后第二个就是我们的传输方式,我们UDP是数据报的形式传输的,就是选我们SOCK_DGRAM,第三个参数我们选0就好。这个就是帮助我们在我们的进程中创建一个文件,返回它的文件描述符。
我们还是要认识到,我们的网络通信还是进程间通信,我们以前学到的进程通信,基本都是文件进行通信,所以我们以后的网络通信,还是要借助文件的,Linux下一切皆文件的设计哲学贯穿我们学习的始终,我们的网卡也是一个文件呀!键盘也是文件,Linux一下皆文件,我们把知识复用就比较容易理解。
我们Bind就是对我们的ip和端口进行初始化,如果我们是客户端,我们要指定我们要发送的服务端的IP和端口,你要把你发送的目标交给我。
第一个参数就是我们的文件下标,第二个参数就是我们的网络通信要的结构体,第三个是长度,它是一个输入输出的参数。
我们的sockaddrf_in就是一个定义的结构体,我们需要对它的内容进行填充,有端口,有IP
至此我们在进程内创建了进行UDP通信的文件,我们还知道了这个文件的文件描述符,以后我的通信就要往对应的文件里写或者读,然后我又初始化了我的端口和IP,接下来准备工作完毕,我们就需要开始读和写就可以了。
读的时候,我们要拿到对端的IP和端口,不然我拿到了消息,我要写回我不是写不回去了吗?所以读的时候我们需要定义一个结构体,我们让对我们的对端进行读取。方便写入。
我们可以得出结论,我们的sockfd是可读可写,UDP支持全双工。它可以读的时候写,写的时候读,这是因为它有2个文件缓冲区,读一个缓冲区,写一个缓冲区。
我们实践中,我们的服务器端绑定端口和IP的时候,只会绑定固定的端口,但不会绑定固定的IP,因为一个服务器会有多个IP如果我们绑定,我们就只能通过指定IP访问我的服务器,在我们的客户端我们不用主动去绑定我们的IP和端口,因为你像,我一个客户端同时要和多个服务端进行通信,如果我主动绑定,有概率出现冲突,所以我们需要让我们的服务器去帮助我们隐式的绑定我们的IP和端口。细节聊完,我们要进行我们的代码实现,来进行我们的实操。我认为,只有理论与实践相结合才能充分了解一个东西,不然很容易故步自封。
下面我们简单实现一下,先写我们的服务端:
服务端我们用解耦合的方式来写,客户端我们直接写。
服务端的主函数:
// 服务端
#include <string>
#include <iostream>
#include <memory>
using namespace std;
#include"udpserve.hpp"
void usuage(std::string proc)
{std::cerr << "Usage: " << proc << " localport" << std::endl;
}
int main(int argc, char *argv[])
{if (argc != 2){usuage(argv[0]);return 0;}uint16_t port = std::stoi(argv[1]); // 端口号// 不指定设定IP。std::unique_ptr<udpserve> usvr = std::make_unique<udpserve>(port);usvr->init();usvr->start();return 0;
}
服务端的实现:
我们初始化要进行套接字的创建,然后进行端口的绑定,绑定的时候不绑定固定的IP理由上面说过,然后我们要注意我们在对我们local进行初始化的时候要注意我们要先进行清空,再进行写入。
原因如下;
bind()
函数的作用是将套接字与一个具体的本地IP地址和端口号绑定。sockaddr_in
结构体中有些字段你可能不会显式设置,但内核在调用 bind()
时会读取整个结构体的所有内存内容。如果里面有残留的随机数据(“垃圾值”),会导致不可预知且非常诡异的错误。
初始化后,我们要让我们的服务跑起来,进行读取,让对端发消息我来读,然后我再做出对应的应答。读取的时候我们要读取对端的iP和端口,我们响应的时候才能进行响应。
class udpserve
{public:udpserve(uint16_t port):_port(port){}void init()//创建fd并且bind{_sockfd=socket(AF_INET,SOCK_DGRAM,0);//创建套接字//绑定套接字struct sockaddr_in local;// memset(&local,0,sizeof(local));local.sin_port=htons(_port);local.sin_family=AF_INET;local.sin_addr.s_addr=htonl(INADDR_ANY);bind(_sockfd,(struct sockaddr*)&local,sizeof(local));cout<<"intit success"<<endl;}void start(){//首先要读取消息cout<<"start"<<endl;while(true){struct sockaddr_in temp;memset(&temp,0,sizeof(temp));socklen_t len=sizeof(temp);char buffer[1024];buffer[0]=0;ssize_t n= recvfrom(_sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&temp,&len);//读取消息到buffer并且得到客户端的ip+port在temp里uint16_t clientport=ntohs(temp.sin_port);//网络转成本地string clientip=inet_ntoa(temp.sin_addr);//读到我们的客户端ip和端口号把它们转成本地cout<<"clientip"<<clientip<<"clientport:"<<clientport<<"client enho# "<<buffer<<endl;//发送消息buffer[n]=0;string echo_string = "server echo# ";echo_string += buffer;//发送的时候要把我们的发送对象标出来sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&temp,len);}}private:int _sockfd;uint16_t _port;//端口号
};
接下来是客户端:
我们的客户端也是创建套接字,不需要绑定,让系统帮助我们进行隐式的绑定,然后进行消息发送,读取。
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
//客户端void useuage(const char*proc)
{std::cerr << "Usage: " << proc << " serverip serverport" << std::endl;
}int main(int argc,char*argv[])
{//我们argv里面要有我们服务端的ip和这个端口号,这样我可以知道我们客服端要发给哪个ip+portif(argc!=3){useuage(argv[0]);exit(0);}string serverip=argv[1];uint16_t serverport=stoi(argv[2]);int sockfd=socket(AF_INET,SOCK_DGRAM,0);//创建套接字,ip+port.if(sockfd<0)//创建失败{cout<<"创建套接字失败"<<endl;return 0;}//接下来我们的问题是要不要bind,我们给我们的ip和port进行赋值,不需要显示绑定port,不需要会出现端口冲突struct sockaddr_in server;memset(&server,0,sizeof(server));server.sin_family=AF_INET;server.sin_port=htons(serverport);server.sin_addr.s_addr=inet_addr(serverip.c_str());while(true){std::cout << "Please Enter@ ";std::string line;std::getline(std::cin, line);// 发送消息sendto(sockfd,line.c_str(),line.size(),0,(struct sockaddr*)&server,sizeof(server));//读取消息struct sockaddr_in temp;socklen_t len=sizeof(temp);char buffer[1024];int m=recvfrom(sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&temp,&len);if(m>0){//读取消息成功buffer[m]=0;cout<<buffer<<endl;}}return 0;
}
这里我们读取的时候就需要创建一个结构体对我们的对端的IP和端口进行读取,因为我们的客户端可能同时和多个服务端进行通信,那么我们就需要对我们的对端IP和端口了,还有就是我们的读取就需要读取对端的IP和端口,无论是在客户端还是我们的服务端。
我们的服务端和客户端通信的效果如下: