计算机网络之套接字编程

端口 && IP

在学习套接字编程之前,我们必须了解一下前缀知识。首先是IP和端口的作用。

在这之前,我们要明白一件事。那就是把数据从一台主机发送到另一台主机,是目的吗???当然不是!!我们要把数据从一台主机发送到另一台主机指定中的一个进程,再由这个进程对数据进行处理后返回给用户。这才是目的!!那么我们通过IP找到了指定的主机,如何找到主机上指定的进程呢?? 那么我们就需要用到端口号(port)。

IP : 标识全网唯一一台主机(不太准确,暂时这么理解)

port 端口号 : 标识主机中唯一的一个进程

所以,IP + port 是不是就可以确定全网唯一 一个进程? 因为 IP确定全网唯一一台主机,端口号确定主机上唯一的一个进程。所以 IP + port 就可以确定全网唯一 一个进程。

而这个时候,我们主机与主机之间的通信,就变成了进程与进程之间的通信!进程间通信的本质是什么?是让多个进程可以看到同一份资源!在本地主机中这份资源可以是内存,也可以是文件。但是在多台主机中,这份资源就是我们熟悉的网络!!

所以我们可以用server把数据发送到网络中,再由对端的client从网络中读。就类似于读文件和写文件的操作一样,就可以实现跨网络的进程间通信!!

注意!

端口一旦确定,那么就不能改变!!因为在对应的客户端必须要指定服务器的端口,而此时如果服务器改变了自己的端口号,那么客户端也必须做到相应的改变!但显然客户并不会意识到这一点,所以端口号一旦确定,不要轻易改变!!!

TCP && UDP

接下来我们再浅浅了解一下TCP和UDP 。

TCP协议的特点

  • 传输层协议
  • 会建立连接
  • 可靠传输
  • 面向字节流

UDP协议的特点

  • 传输层协议
  • 不建立连接
  • 不可靠传输
  • 面向数据报

有连接 && 无连接

显然,UDP和TCP都是传输层协议,那么建立连接这个怎么理解呢?这就很类似两个人打电话,必须要先有一方给另一方播电话。而另一方看到电话来了只有在接通之后两人才能进行通信。而这个过程就类似于TCP的连接过程。

而UDP是无连接的,就意味着你给对方发送数据压根就不用等对方确认。就相当于你给对方打电话,对方不能自己接通,而是电话自动接通。这就是无连接 。

可靠传输 && 不可靠传输

首先我们要知道,可靠和不可靠在这里都是一个中性词。并不是一个贬义词,因此不能因为UDP是不可靠传输就说UDP差。我们要知道,保证可靠性是有代价的!! 因为TCP提供了各种可靠性机制,所以这也就意味着TCP会非常复杂!而UDP是不可靠性传输,就意味着它比较简单。各有各的优势,还是看场景进行选择。

注意!!无论是UDP和TCP,没有明显的谁慢谁快。因为跨网络传输看的是两台主机之间的距离和当前的网络状态!!与协议关系并不大。

面向字节流 && 面向数据报

面向字节流就是以字节流的形式进行报文的发送,那么这就意味着发送的时候,报文可能并不完整!比如你主机的传输层缓冲区已经满了,但是你的报文只接收了前面一点点。就只能等缓冲区被提取后才会接收后面的数据。这种现象就是面向字节流的!

面向数据报则是每次发送/接收,都必须是完整的报文!不存在断截之后再续上的现象,这就叫做面向数据报传输!!

网络字节序

我们都知道内存的字节存储方式有大端和小端。

大端存储 高位放在低字节序,低位放在高字节序 如:0X11223344 在内存中存储是 11 22 33 44

小端存储 低位放在低字节序,高位放在高字节序 如:0X11223344 在内存中存储是 44 33 22 11

既然不同的机器有不同的存储方式,那你怎么保证你小端发送的数据和大端接收的数据是匹配的?

很简单!!只要规定在网络上传输的数据都必须按大端字节序来存储!!发送发如果是小端则只需要转到大端发送即可,接收方是小端再由大端转回小端即可,如果是大端则不需要操作。

但是如果让我们自己来转换,那是不是太麻烦了呢?所以操作系统提供了下面的接口来供我们转换序列。

c 复制代码
#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong); //主机转网络序列,转成long类型
uint16_t htons(uint16_t hostshort); //主机转网络序列,转成short类型
uint32_t ntohl(uint32_t netlong); //网络序列转主机序列,转成long类型
uint16_t ntohs(uint16_t netshort); //网络序列转主机序列,转成short类型

其中的h代表主机host,to就是到的意思,n则是network网络的意思。l对应long,s对应short。所以上面的函数其实很好记忆。

Socket套接字编程

Socket是一个套接字,它的本质就是一个文件描述符。对应的是网卡文件,我们只需要往文件描述符里面写入,就可以把数据发送到网络。

函数介绍

套接字的创建(UDP/TCP/服务端/客户端)

c 复制代码
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

int domain : 通信范围,例如AF_UNIX,AF_LOCAL是本地通信,AF_INET是IPV4,AF_INET6是IPV6 int type: 通信类型,最常见的是TCP的SOCK_STREAM面向字节流,UDP的SOCK_DGRAM面向数据报 int protocol: 确定socket支持的哪个协议,一般默认为0即可。 返回值:一个文件描述符,小于0则代表创建套接字(打开文件)失败

端口号绑定(UDP/TCP/服务端/客户端)

对套接字绑定端口,一般服务器显示绑定。客户端OS自动绑定。

c 复制代码
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);

int sockfd: 要绑定的套接字文件描述符 const struct sockaddr *addr :一个结构体的指针,该结构体包含了domain,port,ip等信息.... socklen_t addrlen:结构体体长度 返回值:成功返回0,绑定失败返回-1,或者返回错误码

这个结构体的首元素是一个16位地址类型,虽然下面的内容都不一样,但只要统一强转成 struct sockaddr,那么就可以拿到前16位。再根据前16位判断出结构体的类型,再做出对应的处理。我们可以把sockaddr 看成是一个父类,sockaddr_in 和 sockaddr_un 是它的子类。而无论我们是使用sockaddr_in还是sockaddr_un,我们都可以用sockaddr来接收。再根据前16位判断地址类型,做出选择。这就很像我们的多态,因为这个功能出来的时候,C语言没有void*指针这个功能,所以我们传入时必须要进行类型强转。

监听套接字(TCP/服务端)

UDP不会用到监听套接字这个函数,因为UDP是无连接的。而TCP是有连接的,所以必须把这个套接字设置为监听状态。

c 复制代码
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);

int sockfd:要监听的套接字 int backlog:连接同时存在的最大数量(因为监听是有消耗的,如果大量连接过来,监听不过来了,那么就会把这些连接暂时存储起来,而backlog就是存储的最大数量。) 返回值: 成功返回0,监听失败返回-1,或者返回错误码

接收连接请求(TCP/服务端)

c 复制代码
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int sockfd  监听后的套接字

struct sockaddr *addr: 存储对端domain,port,ip的结构体 socklen_t *addrlen: 结构体的大小 返回值: 一个可以直接通信的套接字

建立连接(TCP/客户端)

c 复制代码
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);

int sockfd:的套接字

const struct sockaddr *addr: 对端的addr结构体,可以提取到对端的IP和端口

socklen_t addrlen: 要接收的长度。

UDP通信测试

介绍2个函数,UDP传输要用的两个函数。 因为udp是无连接的,所以需要特定的函数来进行通信。

sendto函数

c 复制代码
#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);

该函数的功能是往网络套接字中发送数据,如果调用该函数时套接字还没有绑定端口,那么该函数会自动为套接字随机分配端口。

int sockfd: 套接字

void *buf, size_t len: 发送的数据

size_t len:发送数据的大小

int flags:发送方式,0为阻塞发送

const struct sockaddr *dest_addr:该参数包含着目标主机的IP和端口,根据该参数找到服务器

socklen_t addrlen:传入的sockaddr的长度

recvfrom函数

cpp 复制代码
#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);

该函数的功能是往从网络套接字中读取数据。

int sockfd: 套接字

void *buf, size_t len 读取数据的缓冲区

size_t len:读取数据的大小

int flags:是否阻塞读取,0为阻塞

const struct sockaddr *dest_addr:输出型参数,该参数对端主机的IP和端口,可以用来获取对端的主机IP和端口

socklen_t addrlen:接收的sockaddr 的长度

udp代码演示

服务器代码

server.cc文件

cpp 复制代码
#include "server.hpp"
#include <memory>

int main(int argc , char* argv[])
{
    if(argc != 2) //命令行参数不为2就退出
    {
        std::cout << "Usage : " << argv[0] << "   bindport" << std::endl;  //打印使用手册
        exit(1);
    }
    uint16_t port = atoi(argv[1]); //命令行传的端口转换成16位整形
    std::unique_ptr<UdpServer> s(new UdpServer(port)); //创建UDP服务器
    s->init(); //初始化服务器,创建 + 绑定
    s->start(); //运行服务器
}

server.hpp 代码:

cpp 复制代码
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <iostream>
#include <unistd.h>
#include <arpa/inet.h>

class UdpServer
{
private:
    int _sock; 
    uint16_t _port;
public:
    UdpServer(uint16_t port): _port(port) { }
    ~UdpServer() { close(_sock); }
    void init()
    {
        _sock = socket(AF_INET,SOCK_DGRAM,0);  //创建套接字
        if(_sock < 0)
        {
            //创建失败
            std::cout << "create socket failed...." << std::endl;
            abort();
        }
        //绑定 
        struct sockaddr_in ser; 
        ser.sin_port = htons(_port);  //填入端口
        ser.sin_family = AF_INET; // 填入地址类型
        ser.sin_addr.s_addr = INADDR_ANY; //填入IP地址
        if(bind(_sock,(sockaddr*)&ser,sizeof ser) != 0) //绑定
        {
            //绑定失败
            std::cout << "bind socket failed...." << std::endl;
            abort();
        }
    }

    void start()
    {
        struct sockaddr_in peer; //对端
        socklen_t peer_len = sizeof peer;
        char buff[1024] = {0};   
        while(1)
        {
            //接收对端发来的消息
            int n = recvfrom(_sock,buff,1023,0,(struct sockaddr*)&peer,&peer_len); 
            buff[n] = 0;
            if(read == 0) //对端关闭了
            {
                std::cout << "one client quit..." << std::endl;
                continue;
            }else if(read < 0) //读取出错了
            {
                 std::cout << "read error..." << std::endl;
                 break;
            }
            //读取正常...
            std::string clientip = inet_ntoa(peer.sin_addr); //获取对端的IP
            uint16_t clientport = ntohs(peer.sin_port);// 获取对端的端口
            //打印信息....
            std::cout << "["<< clientip <<"] "<< clientport << ": say : " << buff << std::endl; 
            
        }
    } 
};

客户端代码

client.cc:

cpp 复制代码
#include "client.hpp"
#include <memory>

int main(int argc , char* argv[])
{
    if(argc != 3) //命令行参数少于3个退出,因为必须 ./client 服务器ip 端口号  才可以调用
    {
        std::cout << "Usage : " << argv[0] << "   serverip  serverport" << std::endl; 
        exit(1);
    }
    uint16_t port = atoi(argv[2]); //获取服务器端口
    std::string ip = argv[1]; //获取服务器IP
    std::unique_ptr<UdpClient> cli(new UdpClient(port,ip)); //创建客户端
    cli->init(); //初始化客户端
    cli->start(); //客户端运行
}

client.hpp

cpp 复制代码
#pragma once
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <iostream>
#include <arpa/inet.h>

class UdpClient
{
public: 
    UdpClient(uint16_t port , const std::string& ip) : _port(port), _svr_ip(ip){}
    ~UdpClient(){ close(_sock); }

    void init()
    {
        //创建套接字
         _sock = socket(AF_INET,SOCK_DGRAM,0); 
        if(_sock < 0)
        {
            std::cout << "create socket failed...." << std::endl;
            abort();
        }   
    }
    void start()
    {
        struct sockaddr_in svr; //该结构体填入服务器的端口和IP
        svr.sin_port = htons(_port);  //填入端口
        svr.sin_addr.s_addr = inet_addr(_svr_ip.c_str());  //填入IP
        svr.sin_family = AF_INET; //设置地址类型,ipv4
        int i = 1;  //打印数字,随意,可要可不要
        //char buff[1024] = {0};  如果需要读数据可用的输出缓冲区
        while(1)
        {
            std::string message = "hello server " + std::to_string(i++); //构建发送消息
            //发送消息
            sendto(_sock,message.c_str(),message.size(),0,(struct sockaddr*)&svr,sizeof svr);
            //可以通过下面代码再接收服务器发送的数据,如果服务器有发送的话
            // int n = recvfrom(_sock,buff,1023,0,nullptr,nullptr);
            // buff[n] = 0 ;
            // std::cout << "server say# " << buff << std::endl;
            sleep(1); // 睡眠1s
        }
    }

private: 
    int _sock; //套接字
    uint16_t _port; //服务器的端口
    std::string _svr_ip; //服务的IP

};
相关推荐
2401_857610031 小时前
SpringBoot社团管理:安全与维护
spring boot·后端·安全
凌冰_1 小时前
IDEA2023 SpringBoot整合MyBatis(三)
spring boot·后端·mybatis
码农飞飞2 小时前
深入理解Rust的模式匹配
开发语言·后端·rust·模式匹配·解构·结构体和枚举
一个小坑货2 小时前
Rust 的简介
开发语言·后端·rust
monkey_meng2 小时前
【遵守孤儿规则的External trait pattern】
开发语言·后端·rust
Estar.Lee2 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
新知图书3 小时前
Rust编程与项目实战-模块std::thread(之一)
开发语言·后端·rust
盛夏绽放3 小时前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js
Ares-Wang4 小时前
Asp.net Core Hosted Service(托管服务) Timer (定时任务)
后端·asp.net
uzong4 小时前
7 年 Java 后端,面试过程踩过的坑,我就不藏着了
java·后端·面试