【计算机网络】Linux环境中的网络套接字编程

简介: 【计算机网络】Linux环境中的网络套接字编程

前言

本编文章是博主学习了网络套接字编程后对相关知识的总结,阅读本文可对网络编程有基本的了解。文章内容包括认识IP地址和端口号的作用、了解网络字节序等网络编程中的基本概念、掌握Socket API的基本用法,从而能够实现简单的UDP客户端/服务器和TCP客户端/服务器,并且理解TCP服务器建立连接、发送数据、断开连接的基本流程。

一、预备知识

理解源IP地址和目的IP地址

IP地址被用来给Internet上的电脑一个编号。大家日常见到的情况是每台联网的PC上都需要有IP地址,才能正常通信。如果一台电脑上的数据要传输给另一台电脑,那么该电脑对应的IP地址就是源IP地址,而对于的目的电脑的IP地址就是目的IP地址。如果一台电脑与目的电脑进行通信,就必须知道目的IP地址,才能将信息发送给正确的对象。

认识端口号

知道了目的IP并不能建立双方之间的通信,两台主机通信的目的不是为了将数据发送给对方,而是为了访问目的主机的某一个访问。而目的主机上通常不仅仅只有一个服务,因此就引入了端口号进行区分。


所谓的端口,就好像是门牌号一样,客户端可以通过IP地址找到对应的服务器端,但是服务器端是有很多端口的,每个应用程序对应一个端口号,通过类似门牌号的端口号,客户端才能真正的访问到该服务器。为了对端口进行区分,将每个端口进行了编号,这就是端口号。

端口号的作用

端口号的主要作用是表示一台计算机中的特定进程所提供的服务。网络中的计算机是通过IP地址来代表其身份的,它只能表示某台特定的计算机,但是一台计算机上可以同时提供很多个服务,如数据库服务、FTP服务、Web服务等,我们就通过端口号来区别相同计算机所提供的这些不同的服务,如常见的端口号21表示的是FTP服务,端口号23表示的是Telnet服务端口号25指的是SMTP服务等。端口号一般习惯为4位整数,在同一台计算机上端口号不能重复,否则,就会产生端口号冲突这样的例外。

端口号的使用规则

TCP与UDP段结构中端口地址都是16比特,可以有在 0 ~ 65535 范围内的端口号。对于这65536个端口号有以下的使用规定:

  1. 端口号小于256的定义为常用端口,服务器一般都是通过常用端口号来识别的。任何TCP/IP实现所提供的服务都用1 ~ 1023之间的端口号,是由ICANN来管理的;端口号从1024—49151是被注册的端口,也成为“用户端口”,被IANA指定为特殊服务使用;
  2. 客户端只需保证该端口号在本机上是唯一的就可以了。客户端端口号因存在时间很短暂又称临时端口号;
  3. 大多数TCP/IP实现给临时端口号分配1024—5000之间的端口号。大于5000的端口号是为其他服务器预留的。

认识UDP协议和TCP协议

在TCP/IP网络体系结构中,TCP(传输控制协议,Transport Control Protocol)、UDP(用户数据报协议,User Data Protocol)是传输层最重要的两种协议,为上层用户提供级别的通信可靠性。

传输控制协议(TCP)


TCP(传输控制协议)定义了两台计算机之间进行可靠的传输而交换的数据和确认信息的格式,以及计算机为了确保数据的正确到达而采取的措施。协议规定了TCP软件怎样识别给定计算机上的多个目的进程如何对分组重复这类差错进行恢复。协议还规定了两台计算机如何初始化一个TCP数据流传输以及如何结束这一传输。TCP最大的特点就是提供的是面向连接、可靠的字节流服务。

用户数据报协议(UDP):

UDP(用户数据报协议)是一个简单的面向数据报的传输层协议。不提供可靠性,也不提供报文到达确认、排序以及流量控制等功能。它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。因此报文可能会丢失、重复以及乱序等。但由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。UDP最大的特点就是提供的是非面向连接的、不可靠的数据流传输。

了解网络字节序

通过以前对C语言的学习,我们知道了内存中的多字节数据相对于内存地址有大端和小端之分(大端和小端),磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。当然,网络数据流中也同样有大小端之分。

以下是对网络数据流的地址定义

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机从网络中接收到的数据依次保存到缓冲区中,也同样按照内存地址从低到高的顺序保存;
  • 由此可以保证网络数据流的地址都是:先发出的数据在低地址处,后发出的数据在高地址处;
  • TCP/IP协议规定,网络数据流采用大端字节序,即低地址高字节;
  • 不管当前主机是大端机还是小端机,都会按照TCP/IP协议所规定的网络字节序进行接收就发送数据;
  • 如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可。

数据采用大小端写入内存的区别:

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转

换。

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
  • 这些函数名很好记,h表示host;n表示network;l表示32位长整数;s表示16位短整数。
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

二、socket 套接字

socket 常见API

#include <sys/socket.h>
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及UNIX Domain Socket等。然而,各种网络协议的地址格式并不相同。

sockaddr 和 sockaddr_in

struct sockaddrstruct sockaddr_in 这两个结构体用来处理网络通信的地址。

1、sockaddr

sockaddr在头文件#include <sys/socket.h>中定义,sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了。其结构如下:

/* POSIX.1g specifies this type name for the `sa_family' member.  */
typedef unsigned short int sa_family_t;
/* This macro is used to declare the initial common members
   of the data types used for socket addresses, `struct sockaddr',
   `struct sockaddr_in', `struct sockaddr_un', etc.  */
#define __SOCKADDR_COMMON(sa_prefix) \
  sa_family_t sa_prefix##family
/* Structure describing a generic socket address.  */
struct sockaddr
  {
    __SOCKADDR_COMMON (sa_);  /* Common data: address family and length.  */
    char sa_data[14];   /* Address data.  */
  };
struct sockaddr {  
     sa_family_t sin_family;//地址族
    char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息               
   }; 

2、sockadd_in

sockaddr_in在头文件#include<netinet/in.h>#include <arpa/inet.h>中定义,该结构体解决了sockaddr的缺陷,把portaddr 分开储存在两个变量中,其结构定义如下:


/* Structure describing an Internet socket address.  */
struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;     /* Port number.  */
    struct in_addr sin_addr;    /* Internet address.  */
    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr)
         - __SOCKADDR_COMMON_SIZE
         - sizeof (in_port_t)
         - sizeof (struct in_addr)];
  };

sin_port和sin_addr都必须是网络字节序(NBO),一般可视化的数字都是主机字节序(HBO)。


虽然socket API中的接口是sockaddr,但是我们真正在基于IPv4编程时,使用的数据结构是sockaddr_in,这个结构里主要有三部分信息:地址类型、端口号、IP地址。


3、sockaddr 和 sockaddr_in的结构

  • IPv4和IPv6的地址格式定义在头文件netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址。
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6, 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
  • socket API 可以都用struct sockaddr *类型表示,在使用的时候需要强制转化成sockaddr_in, 这样的好处是程序的通用性,既可以接收IPv4、IPv6、也可以接收UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。

4、总结

二者长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。


sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址。

sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作,使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数。即sockaddr_in用于socket定义和赋值;而sockaddr用于函数参数。

用法举例:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(int argc,char **argv)
{
    int sockfd = 0;
    struct sockaddr_in addr_in;
    struct sockaddr * addr;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);  //获得fd
    bzero(&addr_in,sizeof(addr_in));  // 初始化结构体
    /*
     8008的主机字节序  小端字节序 0001 1111 0100 1000 = 8008
     8008的网络字节序  大端字节序 0100 1000 0001 1111 = 18463
    */
    addr_in.sin_port = htons(8008);
    addr_in.sin_family = AF_INET;  // 设置地址家族
    addr_in.sin_addr.s_addr = inet_addr("192.168.3.30");  //设置地址
    printf("sockaddr_in.sin_addr.s_addr = %d \n", addr_in.sin_addr.s_addr);
    printf("addr = %s \n", inet_ntoa(addr_in.sin_addr));
//    addr_in.sin_addr.s_addr = htonl(INADDR_ANY);  //设置地址
    printf("struct sockaddr size = %ld \n", sizeof (addr));
    printf("struct sockaddr_in size = %ld \n", sizeof (addr_in));
    addr = (struct sockaddr *)&addr_in;
//    bind(sockfd, (struct sockaddr *)&addr_in, sizeof(struct sockaddr));  /* bind的时候进行转化 */
    bind(sockfd, addr, sizeof(struct sockaddr));
    ... ...
    return 0;
}

其中inet_addr()作用是将一个IP字符串转化为一个网络字节序的整数值,用于sockaddr_in.sin_addr.s_addr的初始化。inet_ntoa()作用是将一个sin_addr结构体输出成IP字符串(network to ascii)。

三、UDP Socket编程

这里我们使用UDP套接字编程实现一个简单的英译汉服务器。

封装UdpSocket

udpSocket.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class udpSocket
{
public:
    udpSocket() : _fd(-1) {}
    ~udpSocket(){}
public:
    // 创建套接字
    bool Socket()
    {
        _fd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET表示采用IPv4, SOCK_DGRAM表示采用udp协议
        if (_fd < 0)
        {
            std::cerr << "create socket error!" << std::endl;
            return false;
        }
        return true;
    }
    // 关闭套接字
    bool Close()
    {
        close(_fd);
        return true;
    }
    // Bind套接字相关信息
    bool Bind(const std::string &ip, uint16_t port)
    {
        sockaddr_in addr;
        // 填充协议家族
        addr.sin_family = AF_INET;
        // 填写端口号,port会通过网络发送给客户端
        addr.sin_port = htons(port);
        // 服务器必须有IP地址,“xx.yy.zz.aaa”,字符串风格的点分十进制 -》4字节IP -> uint32_t IP
        // INADDR_ANY(0): 不关心会bind到哪个IP,bind任意IP,服务器一般的做法
        // inet_addr指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行 h—>n
        addr.sin_addr.s_addr = ip.empty() ? htonl(INADDR_ANY) : inet_addr(ip.c_str());
        // bind 网络信息
        // struct sockaddr_in填充信息, struct sockaddr充当函数参数
        if (bind(_fd, (const sockaddr *)&addr, sizeof addr) == -1)
        {
            std::cerr << "bind error!" << std::endl;
            return false;
        }
        return true;
    }
    // 接收数据
    bool RecvFrom(std::string *buf, std::string *ip = nullptr, uint16_t *port = nullptr)
    {
        char inbuf[1024 * 10];
        sockaddr_in peer;
        socklen_t len = sizeof(peer);
        // 将接收的数据读入到inbuf缓冲区中,并且获取客户端的sockaddr_in信息
        ssize_t read_size = recvfrom(_fd, inbuf, sizeof(inbuf) - 1, 0, (sockaddr *)&peer, &len);
        if (read_size < 0)
        {
            std::cerr << "recvfrom error!" << std::endl;
            return false;
        }
        // 将缓冲区中的内容放到输出型参数中
        buf->assign(inbuf, read_size);
        if (ip != nullptr)
        {
            *ip = inet_ntoa(peer.sin_addr);
        }
        if (port != nullptr)
        {
            *port = ntohs(peer.sin_port);
        }
        return true;
    }
    // 发送数据
    bool SendTo(const std::string &buf, const std::string &ip, uint16_t port)
    {
        sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = ip.empty() ? htonl(INADDR_ANY) : inet_addr(ip.c_str());
        //发送数据给客户端
        ssize_t write_size = sendto(_fd, buf.c_str(), buf.size(), 0, (const sockaddr *)&addr, sizeof(addr));
        if (write_size < 0)
        {
            std::cerr << "sendto error!" << std::endl;
            return false;
        }
        return true;
    }
private:
    int _fd;
};

实现UDP通用服务器

udpServer.hpp

#pragma once
#include "udpSocket.hpp"
// C++11 能够兼容函数指针、仿函数和Lambda表达式
#include <functional>
typedef std::function<void (const std::string &, std::string* resp)> Handler;
class udpServer
{
public:
    udpServer()
    {
        assert(_sock.Socket());
    }
    ~udpServer()
    {
        assert(_sock.Close());
    }
public:
    bool Start(const std::string &ip, uint16_t port, Handler handler)
    {
        // 绑定IP 和 端口号
        if (!_sock.Bind(ip, port))
        {
            return false;
        }
        while (true)
        {
            // 尝试读取请求
            std::string req;
            std::string client_ip;
            uint16_t client_port;
            if (!_sock.RecvFrom(&req, &client_ip, &client_port))
            {
                continue;
            }
            std::string resq;
            //根据请求获得响应
            handler(req, &resq);
            //返回相应给客户端
            _sock.SendTo(resq, client_ip, client_port);
            //debug
            printf("[%s:%d] req: %s, resq: %s\n", client_ip.c_str(), client_port, req.c_str(), resq.c_str());
        }
        _sock.Close();
        return true;
    }
private:
    udpSocket _sock;
};

实现英译汉服务器

dict_server.cpp

#include <iostream>
#include <string>
#include <unordered_map>
#include "udpServer.hpp"
std::unordered_map<std::string, std::string> g_dict;
void Translate(const std::string &req, std::string *resp)
{
    auto it = g_dict.find(req);
    if (it == g_dict.end())
    {
        std::cout << "未查找到!" << std::endl;
        return;
    }
    *resp = it->second;
}
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        printf("Usage: ./dict_server [ip] [port]\n");
        return 1;
    }
    // 数据初始化
    g_dict.insert(std::make_pair("hello", "你好"));
    g_dict.insert(std::make_pair("world", "世界"));
    g_dict.insert(std::make_pair("c++", "最好的编程语言"));
    g_dict.insert(std::make_pair("byte", "字节"));
    // 启动服务器
    udpServer().Start(argv[1], atoi(argv[2]), Translate);
    return 0;
}

实现UDP通用客户端

udpClient.hpp

#pragma once
#include "udpSocket.hpp"
class udpClient
{
public:
    udpClient(const std::string &ip, uint16_t port)
        : _ip(ip), _port(port)
    {
        assert(_sock.Socket());
    }
    ~udpClient() { _sock.Close(); }
public:
    bool RecvFrom(std::string* buf)
    {
        return _sock.RecvFrom(buf);
    }
    bool SendTo(const std::string& buf)
    {
        return _sock.SendTo(buf, _ip, _port);
    }
private:
    udpSocket _sock;
    // 服务器的IP和端口号
    std::string _ip;
    uint16_t _port;
};

实现英译汉客户端

client.cpp

#include <iostream>
#include <string>
#include "udpClient.hpp"
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        printf("Usage: ./dict_server [ip] [port]\n");
        return 1;
    }
    udpClient client(argv[1], atoi(argv[2]));
    while(true)
    {
        std::string word;
        std::cout << "请输入您要查找的单词:";
        if(!(std::cin >> word))
        {
            std::cout << "end..." << std::endl;
            break;
        }
        client.SendTo(word);
        std::string res;
        client.RecvFrom(&res);
        std::cout << word << "的意思是:" << res << std::endl;
    }
    return 0;
}

运行结果:

四、地址转换函数

sockaddr_in中的成员struct in_addr sin_addr表示32位的IP地址,但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换。

字符串转in_addr的函数

#include <arpa/inet.h>
int inet_aton(const char* strptr, struct in_addr* addrptr);
in_addr_t inet_addr(const char* strptr);
int inet_pton(int family, const char* strptr, void* addrptr);


in_addr转字符串的函数

char* inet_ntoa(struct in_addr inaddr);
const char* inet_ntop(int family, const void* addrptr, char* strptr, size_t len);


其中inet_ptoninet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void* addrptr

代码样例:

#include <iostream>
#include <cstdio>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
    sockaddr_in addr;
    inet_aton("127.0.0.1", &addr.sin_addr);
    uint32_t* ptr = (uint32_t*)(&addr.sin_addr);
    printf("addr: %x\n", *ptr);
    printf("addr_str: %s\n", inet_ntoa(addr.sin_addr));
    return 0;
}

运行结果:

关于inet_ntoa

inet_ntoa这个函数返回了一个char*类型的指针,很显然是这个函数自己在内部为我们申请了一块内存来保存IP的结果。那么是

否需要调用者手动释放呢?答案是不需要。


在man手册上说,inet_ntoa函数,是把这个返回结果放到了静态存储区,所以这个时候不需要我们手动进行释放。

目录
相关文章
|
6天前
|
关系型数据库 MySQL Linux
|
9天前
|
Linux
FFmpeg开发笔记(三十四)Linux环境给FFmpeg集成libsrt和librist
《FFmpeg开发实战》书中介绍了直播的RTSP和RTMP协议,以及新协议SRT和RIST。SRT是安全可靠传输协议,RIST是可靠的互联网流传输协议,两者于2017年发布。腾讯视频云采用SRT改善推流卡顿。以下是Linux环境下为FFmpeg集成libsrt和librist的步骤:下载安装源码,配置、编译和安装。要启用这些库,需重新配置FFmpeg,添加相关选项,然后编译和安装。成功后,通过`ffmpeg -version`检查版本信息以确认启用SRT和RIST支持。详细过程可参考书中相应章节。
16 1
FFmpeg开发笔记(三十四)Linux环境给FFmpeg集成libsrt和librist
|
2天前
|
监控 安全 网络安全
云计算环境下的网络安全挑战与对策
【7月更文挑战第13天】在数字时代,云计算以其灵活性、可扩展性和成本效益成为企业IT架构的核心。然而,随着数据和服务迁移到云端,网络安全问题也变得日益复杂。本文探讨了云服务中的主要网络安全风险,包括数据泄露、服务中断和合规性问题,并提出了相应的防护策略,如加强身份认证、数据加密、访问控制和持续的安全监控。通过这些措施,可以有效地提高云环境的安全性,保护关键数据免受威胁。
|
3天前
|
Linux
linux网络统计信息和端口占用情况基本语法
linux网络统计信息和端口占用情况基本语法
|
4天前
|
安全 Ubuntu Linux
6 个受欢迎且好用的轻量级Linux桌面环境
Linux被认为是最安全的系统,但这并不意味着它不受恶意软件或其他安全漏洞的侵害。Linux系统的使用范围非常广泛,因此防范潜在威胁至关重要。在这里,将探索 2024 年适用于 Linux 的最佳防病毒软件。根据评级、功能以及与其他 Linux 发行版的兼容性列出了十款最佳防病毒软件,内容仅供分享,不做其它用途。
47 0
6 个受欢迎且好用的轻量级Linux桌面环境
|
10天前
|
Linux 网络安全 开发工具
linux 常用命令【编程必备】
linux 常用命令【编程必备】
23 4
|
5天前
|
存储 安全 网络安全
云计算环境下的网络安全挑战与对策
在数字化浪潮推动下,云计算服务成为企业信息化的重要支撑。然而,云环境的开放性与复杂性为网络安全带来前所未有的挑战。本文深入探讨了云计算环境中的主要安全风险,包括数据泄露、服务中断和恶意攻击等,并提出了相应的防护措施。通过加强身份认证、数据加密、访问控制及安全审计等手段,可有效提升云服务的安全防护能力。进一步地,文章分析了信息安全管理体系的构建,强调了合规性、风险评估和持续监控的重要性。本文旨在为云计算服务提供商和使用者提供一套实用的网络安全解决方案,以保障业务连续性和数据完整性。
|
6天前
|
存储 安全 网络安全
云计算环境下的网络安全挑战与应对策略
本文将探讨云计算环境中网络安全的重要性,分析云服务中存在的安全风险及其成因,并详细阐述如何通过技术和管理措施来增强云服务的信息安全。文章旨在为云服务提供商和用户提出实用的安全建议,帮助他们构建更为坚固的防御体系。
|
10天前
|
Linux 网络安全 虚拟化
Ngnix04系统环境准备-上面软件是免费版的,下面是收费版的,他更快的原因使用了epoll模型,查看当前Linux系统版本, uname -a,VMWARE建议使用NAT,PC端电脑必须使用网线连接
Ngnix04系统环境准备-上面软件是免费版的,下面是收费版的,他更快的原因使用了epoll模型,查看当前Linux系统版本, uname -a,VMWARE建议使用NAT,PC端电脑必须使用网线连接
|
10天前
|
网络协议 Linux 开发工具
配置Linux固定IP地址,为什么要固定IP,因为他是通DHCP服务获取的,DHCP服务每次重启都会重新获取一次ip,VMware编辑中有一个虚拟网络编辑器
配置Linux固定IP地址,为什么要固定IP,因为他是通DHCP服务获取的,DHCP服务每次重启都会重新获取一次ip,VMware编辑中有一个虚拟网络编辑器