欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

网络套接字socket,利用UDP协议实现服务器与客户端通信

程序员文章站 2022-07-08 08:58:15
...

两台主机之间的通信,是通过网卡再经过网络,互相传输,那么我们先来介绍再通信中想要在全球的pc中找到你要发送数据那一台,就需要IP来标识,那么这里在数据报文中就包含了源IP和目的IP,分别标识的是数据从哪来要到那里去。

有了IP那么要怎么认识是主机的哪个进程收数据,这时就有端口号,一个端口号标识着一台主机上的唯一的进程。那么有个问题?
为什么不用PID而要用端口号?因为在一台主机或者服务器上,一个进程的pid随着系统的重启会发生变化,发生变化了,对于客户端没有什么影响,但是对于服务器,客户端就不知道访问的是哪个进程了,所以用一个传输层的端口号,这样就可以通过绑定的端口号来进行访问。

端口号:端口号是一个两个字节的整数 0 ~ 65535
端口号是用来标识一个进程
一台主机要访问另一台主机,那么就需要IP和端口号来标识一台进程
通常情况下,一个端口号只能被一个进程绑定。

TCP/UDP特点

TCP

  • 传输层协议
  • 有链接
  • 可靠传输
  • 面向字节流

UDP

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

关于网路中传输的字节序

计算机是分有大端和小端。有这个差异是因为计算机制造厂商,那么在网络中传输也就统一了字节序。
小端机,就是在低地址处存地位
大端机,是在高地址处存地位
而网络中规定为大端传输,所以就要在传输的过程中把一些数值转换成网络字节序来进行传输。
那么我们就要了解一组API

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); // 主机转网络int
uint16_t htons(uint16_t hostshort); // 主机转网络short
uint32_t ntohl(uint32_t netlong); // 网络转主机int
uint16_t ntohs(uint16_t netshort); // 网络转主机short

这里再来个linux下的命令netstat -nap 查看

socket套接字

socket套接字是TCP/IP协议族相关的API,返回值为一个文件描述符,我们先来认识socket函数接口

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

参数介绍:
domain :是协议是IPV4,还是IPV6,在当今主要还是IPV4,为4个字节,IPV6为16个字节。
type:是使用TCP还是UDP等等一些协议进行传输
protocol:用来指定socket所使用的传输协议编号。这一参数通常不具体设置,一般设置为0
返回值:
返回一个正整数,是文件描述符。通过对文件描述符的操作来进行网络上数据的传输。

前面我们说了,在网络通信中如何标识一个进程,那就是端口号,所以我们在服务器端就会绑定一个端口号,这样就可以标识出一个进程。通过端口号就可以找到对应的进程进行通信。

介绍绑定端口号API

#include <sys/socket.h>
int bind(int sock, const struct sockaddr* address,\
socklen_t address_len);

参数:
sock:为socket返回的文件描述符
address:结构体里面有IP协议,有要绑定的端口号和IP地址,这是一个通用的结构体,因为c语言没有多态,多以就用一个结构体通过强转来实现自己想要的。
address_len:结构体的大小
返回值:
成功返回0,失败返回-1

有了前面的介绍我们来实现一个简单的服务器与客户端。

UDP实现服务器与客户端之间通信

首先我们介绍我们通信的场景~

我们要实现一个网络版的 +、-、*、/ 计算器
通过客户端发来的信息我们返回一个结果,这个其中我们就自定义了应用层的协议来满足自己的需求。
首先我们来试下服务器
headfile

#pragma once

typedef struct Request
{
    int a;
    int b;
    char s;
}Request;

typedef struct Response
{
    int sum;
}Response;

server.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include "poto.h"

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        perror("usage: ./server [ip] [port]\n");
        return 1;
    }

    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        perror("socket error\n");
        return 2;
    }

    // 绑定端口号
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(argv[1]);
    server.sin_port = htons(atoi(argv[2]));
    int b = bind(sock, (struct sockaddr*)&server, \
    sizeof(server));
    if (b < 0)
    {
        perror("bind error\n");
        return 3;
    }

    // recvfrom
    Request req; // 自定义的协议来进行对请求和响应进行处理
    struct sockaddr_in client;
    socklen_t len = sizeof(client);

////////////////////////////////
// 自定义的协议根据实际情况来进行实际的接受的方式与大小,要注意!!!!
////////////////////////////////

    // sendto
    Response resp;

    while (1)
    {
        // 一般收的函数为输出型参数
        ssize_t recv = recvfrom(sock, &req, sizeof(req), 0, \
        (struct sockaddr*)&client, &len);
        if (recv < 0)
        {
            perror("recvfrom error\n");
            continue;
        }
        if (recv == 0)
        {
            printf("read done!\n");
            return 0;
        }
        // 把网络序转成主机序列
        req.a = ntohl(req.a);
        req.b = ntohl(req.b);

        printf("client %s:%d say # %d%c%d\n", \
               inet_ntoa(client.sin_addr), \
               ntohs(client.sin_port), req.a,req.s, req.b);

        // 进行计算
        switch(req.s)
        {
            case '+':resp.sum = req.a + req.b;
                     break;
            case '-':resp.sum = req.a - req.b;
                     break;
            case '*':resp.sum = req.a * req.b;
                     break;
            case '/':resp.sum = req.a / req.b;
                     break;
            default:;
        }

        printf("server sun = %d\n", resp.sum);
        // 主机序转换成网路序,注意是long
        resp.sum = htonl(resp.sum);

        // 拿到了IP和端口号回发
        sendto(sock, &resp, sizeof(resp), 0, \
        (struct sockaddr*)&client, len);
    }

    return 0;
}

client
这里要说明,客户端一般不需要绑定端口号,在发送数据的时候,操作系统会自动分配端口号。

#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <sys/socket.h>
#include "poto.h"

// .server 127.0.0.1 9999
int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        perror("usage: ./server [ip] [port]\n");
        return 1;
    }

    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        perror("socket error\n");
        return 2;
    }

    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(argv[1]);
    server.sin_port = htons(atoi(argv[2]));

    Request req;
    Response resp;
    while (1)
    {
        printf("输入要计算的数,用空格隔开,如 1 + 2<Enter>\n");
        scanf("%d %c %d", &req.a,&req.s,&req.b);
        if (req.s == '/' && req.b == 0)
        {
            perror("除0非法\n");
            continue;
        }

        req.a = htonl(req.a);
        req.b = htonl(req.b);
        sendto(sock, &req, sizeof(req), 0, \
        (struct sockaddr*)&server, sizeof(server));

        ssize_t rd = recvfrom(sock, &resp, sizeof(resp), 0,\
         NULL, NULL);
        if (rd < 0)
        {
            perror("recvfrom error\n");
            return 3;
        }
        if (rd == 0)
        {
            printf("read done!\n");
            return 0;
        }
        resp.sum = ntohl(resp.sum);
        printf("sum = %d\n", resp.sum);
    }
    return 0;
}

我们来看看运行结构
客户端运行
网络套接字socket,利用UDP协议实现服务器与客户端通信
服务器运行
网络套接字socket,利用UDP协议实现服务器与客户端通信