网络套接字编程

网络套接字编程

一、 认识UDP协议

UDP(User Datagram Protocol 用户数据报协议,是不可靠的数据报传输协议,不确保数据安全有序的到达对端。

特点:

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

应用场景:性能要求大于安全要求,比如视频传输。

二、UDP通信程序的编写

0. 前提

a. 什么是套接字编程

就是网络通信程序的编写,网络中的各种通信,都是用户与服务器之间的通信,不存在用户与用户之间的直接通信。也不存在服务器和服务器之间的直接通信。

b. 网络通信都是两端主机之间的通信:客户端,服务端。

客户端:网络通信中用户的一端,是进行业务请求的一端,是主动发起请求的一端。

服务端:网络通信中提供服务的一端,针对客户端请求进行处理的一端,是被动接收请求的一端。

客户端要给服务端发送数据,客户端怎么知道要发送给谁呢?

  • 服务端都会提前将自己的地址信息封装在客户端中,也正是因为如此,客户端的地址信息通常都不能改变。
c. 网络传输的数据都会具有五元组
  • sip:源端IP
  • sport:源端端口
  • dip:对端IP
  • dport:对端端口
  • protocol:协议

五元组标识了一条通信:数据从哪来,到哪去,用的什么协议。

1. 了解通信流程

a. 客户端
  1. 创建套接字:在内核中创建了一个套接字结构(struct socket),用于关联网卡与当前通信进程。
  2. 绑定地址信息:不建议进行,因为一旦绑定了地址,发送数据的源端地址就是固定的,但是一个端口只能被一个进程占用,容易冲突。
  3. 发送数据:将数据放到发送缓冲区,并告诉套接字这些数据要发送给谁(对端地址),系统在封装的时候就会发现数据没有源端地址,这时操作系统就会自动选择一个合适的地址信息进行绑定。
  4. 接收数据:从套接字的接收缓冲区取出数据。
  5. 关闭套接字:释放资源。
b. 服务端
  1. 创建套接字:在内核中创建了一个套接字结构(struct socket),用于关联网卡与当前通信进程。
  2. 为套接字绑定地址信息:描述sip和sport到socket中,用于告诉操作系统当收到数据时,这个数据要发送给所描述的sip和sport的时候,要交给这个socket进行处理。
  3. 接收数据:从套接字的接收缓冲区中取出数据,顺便获取这个数据是谁发的(源端地址)。
  4. 发送数据:将要发送的数据放入到发送缓冲区中,并且告诉套接字这个数据要发送给谁。
  5. 关闭套接字:释放资源。

2. 认识通信接口

  1. 创建套接字

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

    domain:地址域类型,用于决定通信使用什么地址结构,IPV4地址域类型则是AF_INET。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1jl2eIjl-1643444904654)(C:\Users\han\AppData\Roaming\Typora\typora-user-images\image-20220121214410814.png)]

    type:套接字类型,决定使用什么套接字传输方式。

    • SOCK_STREAM:流式套接字,基于连接的,有序的,可靠的字节流传输服务。

    • SOCK_DGRAM:数据报套接字,无连接的,不可靠的数据报传输服务。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5sd5dI53-1643444904655)(C:\Users\han\AppData\Roaming\Typora\typora-user-images\image-20220121214751938.png)]

    protocol:使用的协议类型,流式套接字默认0则表示TCP协议,数据报套接字默认0则表示UDP协议。

    返回值:套接字操作句柄(文件描述符),失败返回-1。

  2. 绑定地址信息

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

    sockfd:创建套接字返回的操作句柄,用于决定给哪个套接字绑定地址。

    addr:绑定地址的信息。

    len:地址信息长度。

    **注意:**struct sockaddr是一个通用的地址接口,在真正使用的时候并不会使用它,而是使用一个具体的通信地址结构,然后强转其类型并传入数据即可,bind接口内部会根据传入数据的前2个字节决定这个传入的地址数据该如何解析。len是地址信息长度,作用是第二个参数传入的是地址,所以要指定访问的长度,防止访问越界。

    返回值:成功返回0,失败返回-1.

  3. 发送数据

    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

    sockfd:创建套接字返回的操作句柄。

    buf:要发送的数据的首地址(socket并不关心发送数据的具体内容)。

    len:要发送的数据长度。

    flags:标志位,0是默认阻塞操作。

    dest_addr:目的端地址信息。

    addrlen:地址信息长度。

    返回值:成功返回实际发送的数据长度,失败返回-1。

  4. 接收数据

    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);

    sockfd:创建套接字返回的操作句柄。

    buf:一块缓冲区的首地址,用于存放获取的数据。

    len:想要发送的数据长度。

    flags:标志位,0是默认阻塞操作。(若缓冲区没有数据则会等待)

    src_addr:接收到的数据的源端地址信息。

    *addrlen:输入输出参数,用于指定要获取的地址长度,以及返回实际长度。

    返回值:成功返回获取到的数据长度,失败返回-1。

  5. 关闭套接字

    int close(int fd);

  6. 字节序转换接口

    网络通信需要使用网络字节序,因此要考虑网络字节序转换问题

    16位数据的主机与网络字节序转换:uint16_t htons(uint16_t hostshort); uint16_t ntohs(uint16_t netshort);

    32位数据的主机与网络字节序转换:uint32_t htonl(uint32_t hostlong); uint32_t ntohl(uint32_t netlong);

    将一个点分十进制的字符串IP地址转换为网络字节序整数IP地址:in_addr_t inet_addr(const char *cp);

    将一个网络字节序整数IP地址转换为点分十进制的字符串IP地址:char *inet_ntoa(struct in_addr in);

3. 编写程序

服务器端:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main(int argc, char* argv[])
{
    //从参数中获取服务端要绑定的地址:包括IP地址和端口号 
    if(argc < 3)
    {
        printf("参数不全\n");
        return -1;
    }
    char* src_ip = argv[1];//获取要绑定的IP地址
    int srv_port = atoi(argv[2]);//获取要绑定的端口

    //1.创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);//AF_INET表示IPV4地址域类型;SOCK_DGRAM表示数据报套接字,0默认是使用UDP协议
    if(sockfd < 0)
    {
        perror("socket error");
        return -1;
    }
    //2.绑定地址信息
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(srv_port);
    addr.sin_addr.s_addr = inet_addr(src_ip);
    socklen_t len = sizeof(addr);
    int ret = bind(sockfd, (struct sockaddr*)&addr, len);
    if(ret < 0)
    {
        perror("bind error");
        return -1;
    }
    //3.接收数据
    while(1)
    {
        char tmp[4096] = {0};
        struct sockaddr_in client_addr;//获取发送端的地址信息
        len = sizeof(client_addr);
        ret = recvfrom(sockfd, tmp, 4095, 0, (struct sockaddr*)&client_addr, &len);
        if(ret < 0)
        {
            perror("recvfrom error");
            return -1;
        }
        printf("%s:%d - %s\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), tmp);
        //4.发送数据
        printf("server input:");
        fflush(stdout);
        memset(tmp, 0x00,4096);//清空缓冲区内容
        scanf("%s",tmp);
        ret = sendto(sockfd, tmp, strlen(tmp), 0, (struct sockaddr*)&client_addr, len);
    }
    //5.关闭套接字
    close(sockfd);
    return 0;
}

客户端:

#include <cstdio>
#include <unistd.h>
#include <iostream>
#include <string>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
using namespace std;

class UdpSocket
{
    private:
        int _sockfd;
    public:
        UdpSocket()
            :_sockfd(-1)
        {}
    public:
        //创建套接字
        bool Socket()
        {
            _sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
            if(_sockfd < 0)
            {
                perror("socket error");
                return  false; 
            }
            return true;
        }
        //绑定地址信息
        bool Bind(const string &ip, int port)
        {
            struct sockaddr_in addr;
            addr.sin_family = AF_INET;
            addr.sin_port = htons(port);
            addr.sin_addr.s_addr = inet_addr(ip.c_str());
            socklen_t len = sizeof(struct sockaddr_in);
            if(bind(_sockfd, (struct sockaddr*)&addr, len) < 0)
            {
                perror("bind error");
                return false;
            }
            return true;
        }
        //发送数据
        bool Send(const string &data, const string &ip, int port)
        {
            struct sockaddr_in peeraddr;
            peeraddr.sin_family = AF_INET;
            peeraddr.sin_port = htons(port);
            peeraddr.sin_addr.s_addr = inet_addr(ip.c_str());
            socklen_t len = sizeof(struct sockaddr_in);
            int ret = sendto(_sockfd, &data[0], data.size(), 0, (struct sockaddr*)&peeraddr, len);
            if(ret < 0)
            {
                perror("send error");
                return false;
            }
            return true;
        }
        //客户端实际上是不需要接收服务器的地址的,因为他本来就知道
        bool Recv(string *buf, string *ip = nullptr, int* port = nullptr)
        {
            struct sockaddr_in peeraddr;
            socklen_t len = sizeof(sockaddr_in);
            char tmp[4096] = {0};
            int ret = recvfrom(_sockfd, tmp, 4095, 0, (struct sockaddr*)&peeraddr, &len);
            if(ret < 0)
            {
                perror("recvfrom error");
                return false;
            }
            buf->assign(tmp,ret);//从tmp字符串位置开始截取ret长度的数据到buf中
            if(ip != nullptr)
            {
                *ip = inet_ntoa(peeraddr.sin_addr);
            }
            if(port != nullptr)
            {
                *port = ntohs(peeraddr.sin_port);
            }
            return true;
        }
        //关闭套接字
        bool Close()
        {
            return close(_sockfd);
        }
};

#include"udpsocket.hpp"
#include <cstdlib>
using namespace std;
int main(int argc, char* argv[])
{
    //通过运行参数获取服务端的地址信息
    if(argc < 3)
    {
        cout<<"参数不全"<<endl;
        return -1;
    }
    string srv_ip = argv[1];
    int srv_port = atoi(argv[2]);
    //1.创建套接字
    UdpSocket sock;
    sock.Socket();
    //2.绑定地址信息(不推荐)
    while(1)
    {
        string buf;
        cout << "请输入要发送的内容";
        cin >> buf;
        //3.发送请求
        sock.Send(buf, srv_ip, srv_port);
        //4.接收相应
        buf.clear();
        sock.Recv(&buf);
        cout << buf << endl;
    }
    //5.关闭套接字
    sock.Close();
    return 0;
}


三、认识TCP协议

TCP(Transmission Control Protocol 传输控制协议),是面向连接的,可靠的字节流传输协议(确保了数据安全有序的到达对端,并且建立连接后才可以进行通信),TCP协议为了保证可靠传输,因此使用了很多的机制来完成,因此传输性能相对于UDP协议来说较低。

特点

  • 传输层协议
  • 基于连接的
  • 可靠传输
  • 面向字节流

应用场景:安全需求大于性能需求,比如文件传输。

四、TCP通信程序的编写

1.了解通信流程

a.客户端
  1. 创建套接字:关联网卡与进程。
  2. 绑定地址信息:不推荐主动绑定。
  3. 向服务端发起连接请求:如果没有主动绑定地址则会自动选择合适的地址进行绑定。连接一旦建立成功,客户端的socket中也会具有完整的五元组信息。
  4. 收发数据。
  5. 关闭套接字。
b.服务端
  1. 创建套接字:创建套接字:在内核中创建了一个套接字结构(struct socket),用于关联网卡与当前通信进程。
  2. 绑定地址信息:为套接字绑定地址信息:描述sip和sport到socket中,用于告诉操作系统当收到数据时,这个数据要发送给所描述的sip和sport的时候,要交给这个socket进行处理。
  3. 开始监听:使socket进入listen状态,开始处理客户端连接请求。服务端会为每个新的客户端的连接请求创建一个新的socket,在内部描述完整的五元组信息,这个新建的套接字只与固定的客户端进行通信。
  4. 获取新建套接字的操作句柄:往后与指定的客户端进行通信都是通过新建的套接字(被称为连接套接字)完成的,原本的套接字(被称为监听套接字)只用来新建连接请求。
  5. 收发数据。
  6. 关闭套接字。

2. 通信接口认识

  1. 服务端开始监听

    int listen(int sockfd, int backlog);

    sockfd:套接字描述符。

    backlog:当前服务器在同一时间所能处理的最大的客户端连接请求数量(同一时刻的最大并发连接数)。

    SYN泛洪攻击:恶意主机伪造IP地址,向服务器发送大量的连接请求,这样服务端就会不断创建新的连接套接字,如果服务端对新建套接字的数量不做限制的话,有可能瞬间资源耗尽,系统崩溃。这个限制就是backlog,有了这个限制,遇到SYN泛洪攻击的时候,顶多是无法处理正常的请求,但是不会让系统崩溃,之前的连接还可以正常通信。

    返回值:成功返回0,失败返回-1。

  2. 客户端发送连接请求

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

    sockfd:套接字描述符。

    addr:服务端的地址信息,IPV4通信使用struct sockaddr_in的结构。

    len:地址信息长度。

    返回值:成功返回0,失败返回-1。

  3. 服务端获取新建连接句柄

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

    sockfd:监听套接字描述符(监听套接字描述符仅用来进行新建连接和监听)。

    addr:获取要进行连接的客户端地址信息,描述的是当前要获取的这个套接字是与哪个客户端进行通信的。

    len:输入输出型参数:指定要获取的地址长度,以及返回实际获取的地址长度。

    返回值:成功返回新建连接的套接字描述符,用于后续与客户端进行通信;失败返回-1。

  4. 发送数据:

    ssize_t send(int sockfd, const void *buf, size_t len, int flags);

    sockfd:套接字描述符,对于服务端来说,一定是accept获取到的新建连接的套接字描述符。

    buf:要发送的数据首地址。

    len:要发送的数据首地址。

    flag:标志位,通常置0,表示阻塞发送,就是把数据放到发送缓冲区,系统进行封装发送,如果缓冲区满了则进行等待。

    返回值:成功返回实际发送的数据的长度;失败返回-1。

  5. 接收数据

    ssize_t recv(int sockfd, void *buf, size_t len, int flags);

    sockfd:监听套接字描述符。

    buf:一个缓冲区空间首地址,用于存放接收的数据。

    len:想要获取的数据长度,不能大于buf的缓冲区长度。

    flag:标志位,通常置0,表示阻塞接收,就是socket接收缓冲区中如果没有数据则阻塞。

    返回值:成功返回实际获取到的数据长度;失败返回-1;连接断开返回0。

  6. 关闭套接字

    int close(int fd);

    部分关闭连接

    int shutdown(int sockfd, int how); 这个操作并不会完全释放资源。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RSKNwn6E-1643444904656)(C:\Users\han\AppData\Roaming\Typora\typora-user-images\image-20220128150431045.png)]

    shutdown更多用于进行半关闭连接,让对方知道自己不再发送数据或者不再接收数据了,但是要注意shutdown不是用于关闭套接字释放资源的,就算调用了shutdown,最后也必须使用close关闭释放资源。

3. 程序编写

封装TCPSocket类

#include <iostream>
#include <unistd.h>
#include <string>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define MAX_LISTEN 5
using namespace std;

class TCPSocket
{
    private:
        int _sockfd;
    public:
        TCPSocket()
            :_sockfd(-1)
        {}
        //创建套接字
        bool Socket()
        {
            _sockfd = socket(AF_INET, SOCK_STREAM, 0);
            if(_sockfd < 0)
            {
                perror("socket error");
                return false;
            }
            return true;
        }
        //绑定地址信息
        bool Bind(const string &ip, int port)
        {
            struct sockaddr_in addr;
            addr.sin_family = AF_INET;
            addr.sin_port = htons(port);
            addr.sin_addr.s_addr = inet_addr(ip.c_str());
            socklen_t len = sizeof(addr);
            if(bind(_sockfd, (struct sockaddr*)&addr, len) < 0)
            {
                perror("bind error");
                return false;
            }
            return true;
        }
        //服务端开始监听
        bool Listen(int backlog = MAX_LISTEN)
        {
            if(listen(_sockfd, backlog) < 0)
            {
                perror("listen error");
                return false;
            }
            return true;
        }
        //向服务端发起连接请求
        bool Connect(const string &srv_ip, int srv_port)
        {
            struct sockaddr_in addr;
            addr.sin_family = AF_INET;
            addr.sin_port = htons(srv_port);
            addr.sin_addr.s_addr = inet_addr(srv_ip.c_str());
            socklen_t len = sizeof(addr);
            if(connect(_sockfd, (struct sockaddr*)&addr, len) < 0)
            {
                perror("connect error");
                return false;
            }
            return true;
        }
        //获取新建连接
        bool Accept(TCPSocket* new_sock, string* cli_ip, int* cli_port)
        {
            struct sockaddr_in addr;
            socklen_t len = sizeof(addr);
            int new_fd = accept(_sockfd, (struct sockaddr*)&addr, &len);
            if(new_fd < 0)
            {
                perror("accept error");
                return false;
            }
            new_sock->_sockfd = new_fd;
            if(cli_ip != nullptr)
            {
                *cli_ip = inet_ntoa(addr.sin_addr);
            }
            if(cli_port != nullptr)
            {
                *cli_port = ntohs(addr.sin_port);
            }
            return true;
        }
        bool Send(const string &data)
        {
            ssize_t ret = send(_sockfd, data.c_str(), data.size(), 0);
            if(ret < 0)
            {
                perror("send error");
                return false;
            }
            return true;
        }
        bool Recv(string *buf)
        {
            char tmp[4096] = {0};
            ssize_t ret = recv(_sockfd, tmp, 4096, 0);
            if(ret < 0)
            {
                perror("recv error");
                return false;
            }
            else if(ret == 0)
            {
                cout<<"连接断开"<<endl;
            }
            buf->assign(tmp,ret);
            return true;
        }
        bool Close()
        {
            if(_sockfd > 0)
            {
                close(_sockfd);
                _sockfd = -1;
            }
            return true;
        }
};

客户端

#include "TCPSocket.hpp"

int main(int argc, char* argv[])
{
    if(argc < 3)
    {
        cout<<"参数不全"<<endl;
        return -1;
    }
    string srv_ip = argv[1];
    int srv_port = stoi(argv[2]);


    TCPSocket sock;
    //1.创建套接字
    sock.Socket();
    //2.向服务端发起连接请求
    sock.Connect(srv_ip, srv_port);
    //3、循环收发数据
    while(1)
    {
        string data;
        cout<<"clint input:";
        fflush(stdout);
        cin>>data;
        sock.Send(data);
        data.clear();
        sock.Recv(&data);
        cout<<"server response"<<data<<endl;
    }
    //4.关闭套接字
    sock.Close();
    return 0;
}

服务端

#include "TCPSocket.hpp"
#include <unordered_map>

unordered_map<string, string> table = 
{
    {"hello", "你好"},
    {"goodmorning", "早上好"}
};

int main()
{
    TCPSocket listen_sock;
    //1.创建套接字
    listen_sock.Socket();
    //2.绑定地址信息
    listen_sock.Bind("0.0.0.0", 9000);
    //3.开始监听
    listen_sock.Listen();
    //5.使用新建连接收发数据
    while(1)
    {
        //4.获取新建连接
        TCPSocket new_sock;
        string cli_ip;
        int cli_port;
        listen_sock.Accept(&new_sock, &cli_ip, &cli_port);
        cout<<"new connect"<<cli_ip<<":"<<cli_port<<endl;
        string buf;
        new_sock.Recv(&buf);
        string rsp;
        auto it = table.find(buf);
        if(it == table.end())
        {
            rsp = "未知请求";
        }
        rsp = it->second;
        new_sock.Send(rsp);
    }
    //6.关闭套接字
    listen_sock.Close();
    return 0;
}

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值