【网络】网络编程套接字(一)(2)

简介: 【网络】网络编程套接字(一)(1)

Ⅱ、运行服务器

当服务器初始化完毕后我们就可以启动服务器了,由于服务器是一个永不退出的进程,所以服务器运行以后一定是一个死循环!

读取数据

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:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlensrc_addr结构体的长度,返回时此值会被修改为实际读取到的src_addr结构体的长度,这是一个输入输出型参数。

返回值说明:

  • 读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置。

发送数据

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

功能:

  • 将数据发送到网络中。

参数说明:

  • sockfd:创建的套接字对应的文件描述符,表示将数据写入该文件描述符索引的文件当中。
  • buf:待写入数据的起始地址。
  • len:期望写入数据的字节数。
  • flags:写入的方式,一般设置为0,表示阻塞写入。
  • dest_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:传入dest_addr结构体的长度。

返回值说明:

  • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

现在服务端通过recvfrom函数读取客户端数据,我们可以先将读取到的数据当作字符串看待,将读取到的数据的最后一个位置设置为’\0’,此时我们就可以将读取到的数据进行输出,同时我们也可以将获取到的客户端的IP地址和端口号也一并进行输出。

需要注意的是,我们获取到的客户端的端口号此时是网络序列,我们需要调用ntohs函数将其转为主机序列再进行打印输出。同时,我们获取到的客户端的IP地址是整数IP,我们需要通过调用inet_ntoa函数将其转为字符串IP再进行打印输出。

// udp_server.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
enum
{
    SOCKET_ERR = 1,
    BIND_ERR,
    USAGE_ERR
};
class UdpServer
{
public:
    UdpServer(uint16_t port)
        :_port(port)
    {
        std::cout << "port : " << _port << std::endl;
    }
    void UdpServerInit()
    {
        // 1. 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            std::cerr << "create socket fail : " << strerror(errno) << std::endl;
            exit(SOCKET_ERR);
        }
        std::cout << "create socket success! " << "sockfd : " << _sockfd << std::endl;
        // 2. 填充sockaddr_in结构体
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        // 将主机序列转换为网络序列
        local.sin_addr.s_addr = INADDR_ANY;
        local.sin_port = htons(_port);
        // 3. 绑定IP,端口号
        if (bind(_sockfd, (struct sockaddr*)&local, sizeof(local)) != 0)
        {
            std::cerr << "bind fail :" << strerror(errno) << std::endl;
            exit(BIND_ERR);
        }
        std::cout << "bind success !" << std::endl;
    }
    void UdpServerStart()
    {
      // 缓冲区
        char buf[2048];
        // 网络信息结构体
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        // 死循环不能让服务器退出
        while (true)
        {
            memset(&peer, 0, len);
            // 收取消息
            ssize_t num = recvfrom(_sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&peer, &len);
            if (num < 0)
            {
                std::cerr << "recvfrom fail !" << std::endl;
                continue;
            }
            else
            {
              // 结尾补上\0,形成C风格字符串
                buf[num] = '\0';
            }
            // 提取客户端的ip和端口号
            std::string peer_ip = inet_ntoa(peer.sin_addr);
            uint16_t peer_port = ntohs(peer.sin_port);
            std::cout << peer_ip << " | " << peer_port << " |# " << buf << std::endl;
            // 发消息
            sendto(_sockfd, buf, strlen(buf), 0, (struct sockaddr*)&peer, len);
        }
    }
    ~UdpServer()
    {
        if (_sockfd > 0)
        {
            close(_sockfd);
        }
    }
private:
    int _sockfd;            // 套接字的文件描述符
    uint16_t _port;         // 端口号
};

我们服务器启动的时候需要指定端口号,所以这里使用了命令行参数。

// udp_server.cpp
#include "udp_server.hpp"
#include <iostream>
#include <memory>
// 使用手册
static void usage(std::string proc)
{
    std::cout << "usage\n\t" << proc << " 端口号" << std::endl;
}
// 命令行参数,必须输入两个参数,一个是程序名,一个是端口号
int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    // 提取本地端口号
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<UdpServer> up(new UdpServer(port));
    up->UdpServerInit();
    up->UdpServerStart();
    return 0;
}

程序启动以后我们可以使用netstat -naup显示进程的网络信息。

netstat常用选项说明:

  • -n:直接使用IP地址,而不通过域名服务器。
  • -a: 显示所有连接中的接口信息。
  • -t:显示TCP传输协议的连线状况。
  • -u:显示UDP传输协议的连线状况。
  • -p:显示正在使用Socket的程序识别码和程序名称。

运行结果

查看网络信息

netstat命令显示的信息中:

  • Proto表示协议的类型
  • Recv-Q表示网络接收队列
  • Send-Q表示网络发送队列
  • Local Address表示本地地址,
  • Foreign Address表示外部地址
  • State表示当前的状态
  • PID表示该进程的进程ID
  • Program name表示该进程的程序名称。

其中Foreign Address写成0.0.0.0:*表示任意IP地址、任意的端口号的程序都可以访问当前进程。

Ⅲ、关于客户端的绑定问题

首先,由于是网络通信,通信双方都需要找到对方,因此服务端和客户端都需要有各自的IP地址和端口号,只不过服务端需要显示的进行IP和端口号的绑定,而客户端不需要显示的进行绑定的,这个绑定的工作由操作系统来进行绑定,当我们调用类似于sendto这样的接口时,操作系统会自动给当前客户端获取一个唯一的端口号。

服务器是为了给客户提供服务的,因此服务器必须要让客户知道自己的IP地址和端口号,否则客户端是无法向服务端发起请求的,这就是服务端要进行显示绑定的原因,只有一个进程绑定了端口号之后这个端口号才真正属于自己,因为一个端口只能被一个进程所绑定,服务器绑定一个端口就是为了独占这个端口。

而客户端在通信时虽然也需要端口号,但客户端一般是不进行绑定的,客户端访问服务端的时候,端口号只要是唯一的就行了,不需要明确是那个特定的端口号。

一台设备上可以运行很多客户端,例如:B站客户端绑定了8080端口号,那么以后8080端口号就只能给B站客户端使用,如果8080端口号又被淘宝客户端绑定了并且淘宝先启动了,那么B站客户端就无法启动了,因此客户端端口通常是不绑定,由OS动态分配,也就是说,客户端每次启动时使用的端口号可能是变化的,此时只要我们的端口号没有被耗尽,客户端就永远可以启动。

Ⅳ、启动客户端

客户端的编写与服务端类似,只不过客户端不需要我们进行绑定工作的,此外作为一个客户端,它必须知道它要访问的服务端的IP地址和端口号,因此在我们启动客户端时中需要引入服务端的IP地址和端口号。

客户端和服务端在功能上是相互补充的,我们上面的服务器是在读取客户端发来的数据然后回发回去,那么这里我们的客户端就应该向服务端发送数据,然后接收服务器回发的数据。

// client.cpp
#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
enum
{
    SOCKET_ERR = 1,
    BIND_ERR,
    USAGE_ERR
};
// 使用手册
static void usage(std::string proc)
{
    std::cout << "usage\n\t" << proc << " IP 端口" << std::endl;
}
// 命令行参数,必须输入三个参数,一个是程序名,一个是IP,一个是端口号
int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    // 1. 得到服务器的IP和端口
    std::string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);
    // 2. 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "create socket fail : " << strerror(errno) << std::endl;
        exit(SOCKET_ERR);
    }
    // 3. 填充server结构体
    struct sockaddr_in server;
    socklen_t len = sizeof(server);
    memset(&server, 0, len);
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());
    server.sin_port = htons(server_port);
    // 4. 业务处理
    std::string message;
    char buf[2048];
    while (true)
    {
        std::cout << "[pan的服务器] :> ";
        getline(std::cin, message);
        // 发送消息
        // 在我们首次调用系统调用发送数据时,OS会随机选择一个端口号 + 自己的IP进行bind
        ssize_t num = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);
        if (num < 0)
        {
            std::cerr << "sendto fail !" << std::endl;
            continue;
        }
        struct sockaddr_in temp;
        socklen_t temp_len = sizeof(temp);
        memset(&temp, 0, temp_len);
        // 收消息
        num = recvfrom(sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&temp, &temp_len);
        if (num < 0)
        {
            std::cerr << "recvfrom fail !" << std::endl;
            continue;
        }
        else
        {
            buf[num] = '\0';
        }
        std::cout << "server's message | " << buf << std::endl;
    }
    return 0;
}

Ⅴ、本地测试

现在服务端和客户端的代码都已经编写完毕,我们可以先进行本地测试,现在我们运行服务器时指明端口号为8080,再运行客户端,此时客户端要访问的服务器的IP地址就是本地环回127.0.0.1地址,服务端的端口号就是8080。

  • 127.0.0.1 :本地环回,表示当前主机的地址,通常用来进行本地通信或测试。

我们要让服务端先运行,然后再让客户端运行,之后提示我们进行输入,当我们在客户端输入数据后,客户端将数据发送给服务端,此时服务端再将收到的数据打印输出后回发,这时我们在服务端和客户端的窗口都能看到我们输入的内容。

此时我们再用netstat命令查看网络信息,可以看到服务端的端口是8080,客户端的端口是44777。这里客户端能被netstat命令查看到,说明客户端也已经动态绑定成功了,这就是我们所谓的网络通信。

Ⅵ、网络测试

如果你是云服务器,请确保你想使用的端口已经开放,下面是腾讯云的云服务器开放端口的方法:

好了,我们开始进行网络测试:

你可以将此客户端软件给更多的人,让它们都能够连接你的服务器,进行网络通信。

相关文章
|
2天前
|
网络协议 Java Linux
【探索Linux】P.29(网络编程套接字 —— 简单的TCP网络程序模拟实现)
【探索Linux】P.29(网络编程套接字 —— 简单的TCP网络程序模拟实现)
10 0
|
2天前
|
存储 网络协议 算法
【探索Linux】P.28(网络编程套接字 —— 简单的UDP网络程序模拟实现)
【探索Linux】P.28(网络编程套接字 —— 简单的UDP网络程序模拟实现)
9 0
|
2天前
|
网络协议 算法 Linux
【探索Linux】P.27(网络编程套接字 —— UDP协议介绍 | TCP协议介绍 | UDP 和 TCP 的异同)
【探索Linux】P.27(网络编程套接字 —— UDP协议介绍 | TCP协议介绍 | UDP 和 TCP 的异同)
9 0
|
2天前
|
存储 算法 网络协议
【探索Linux】P.26(网络编程套接字基本概念—— socket编程接口 | socket编程接口相关函数详细介绍 )
【探索Linux】P.26(网络编程套接字基本概念—— socket编程接口 | socket编程接口相关函数详细介绍 )
9 0
|
2天前
|
存储 网络协议 Unix
【探索Linux】P.25(网络编程套接字基本概念 —— 预备知识)
【探索Linux】P.25(网络编程套接字基本概念 —— 预备知识)
6 0
存储 网络协议 Java
15 3
|
28天前
|
存储 网络协议 算法
网络编程套接字 (一)
网络编程套接字 (一) 网络编程套接字 (一)
29 0
|
13天前
|
网络协议 算法 Linux
【Linux】深入探索:Linux网络调试、追踪与优化
【Linux】深入探索:Linux网络调试、追踪与优化
|
1天前
|
域名解析 网络协议 Linux
linux网络配置详解
linux网络配置详解
10 0
|
6天前
|
Linux Shell 网络安全
网络安全中Dos和linux常用命令总结
本篇是对网安学习中,常用的命令做一个图文与命令示例,并对一些比较重要的dos和shell命令进行总结,方便自己后续学习进行查询,并希望能够给更多人有一个总结命令和了解命令的地方.