网络编程socket(上)(二)

简介: 网络编程socket(上)

三、UDP网络程序

3.1 服务端初始化

3.1.1 服务端创建套接字

将服务器封装成一个类,当定义出一个服务器对象后需要马上初始化服务器,而初始化服务器需要做的第一件事就是创建套接字


socket函数


domain:创建套接字的域(协议家族),即创建套接字的类型。该参数就相当于 struct sockaddr 结构的前16个位。若是本地通信就设置为AF_UNIX,若是网络通信就设置为 AF_INET(IPv4)或 AF_INET6(IPv6)

type:创建套接字时所需的服务类型。其中最常见的服务类型是 SOCK_STREAM 和 SOCK_DGRAM 。若是基于UDP的网络通信,采用的就是SOCK_DGRAM(用户数据报服务);若是基于TCP的网络通信,采用的就是SOCK_STREAM(流式套接字),提供的是流式服务

protocol:创建套接字的协议类别。可以指明为TCP或UDP,但该字段一般设置为0即可,设置为0表示默认,此时会根据传入的前两个参数自动推导出最终需要使用的是哪种协议

返回值:套接字创建成功返回一个文件描述符,创建失败返回-1,同时错误码会被设置


socket函数属于什么类型的接口?


网络协议栈是分层的,按照TCP/IP四层模型,自顶向下依次是应用层、传输层、网络层和数据链路层。而现在所写的代码被称为用户级代码,即是在应用层编写代码,因此调用的实际是下三层的接口,而传输层和网络层都是在操作系统内完成的,所以socket()属于系统调用接口


socket函数底层做了什么?


socket函数是被进程所调用的,而每一个进程在系统层面上都有PCB(task_struct)、文件描述符表(files_struct)以及对应打开的各种文件。而文件描述符表里面包含了一个数组fd_array,其中数组中的0、1、2下标依次对应的就是标准输入、标准输出以及标准错误


100b399272a3421ba3cbb010b88060bb.png


当调用socket函数创建套接字时,实际相当于打开了一个"网络文件",打开后在内核层面上就形成了一个对应的struct file结构体,同时该结构体被连入到了该进程对应的文件双链表,并将该结构体的首地址填入到了fd_array数组当中下标为3的位置,此时fd_array数组中下标为3的指针就指向了这个打开的"网络文件",最后3号文件描述符作为socket函数的返回值返回给了用户


a5d18a1ea74942dc88a56f39fabeb9e0.png


其中每一个struct file结构体中包含的就是对应打开文件各种信息,比如文件的属性信息、操作方法以及文件缓冲区等。其中文件对应的属性在内核当中是由 struct inode 结构体来维护的,而文件对应的操作方法实际就是一堆的函数指针(比如read*和write*)在内核当中就是由 struct file_operations 结构体来维护的。而文件缓冲区对于打开的普通文件而言对应的一般是磁盘,但对于现在打开的"网络文件"而言,文件缓冲区对应的就是网卡

27d6ace7f43345c8b021269a013a94e8.png



对于普通文件而言,当用户通过文件描述符将数据写到文件缓冲区,然后再把数据刷到磁盘上就完成了数据的写入操作。而对于socket函数打开的"网络文件"而言,当用户将数据写到文件缓冲区后,操作系统会定期将数据刷到网卡里面,网卡负责数据发送,数据最终就发送到了网络中


代码实现


进行初始化服务器创建套接字时,就是调用socket函数创建套接字,创建套接字时需要填入的协议家族为AF_INET,因为要进行的是网络通信,而需要的服务类型就是SOCK_DGARM,因为现在编写的UDP服务器是面向数据报的,而第三个参数之间设置为0即可


//UdpServer.h
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
using std::cout;
using std::cerr;
using std::endl;
class UdpServer
{
public:
    bool InitServer();
    ~UdpServer();
private:
    int _socket_fd;
};


//UdpServer.cc
#include "UdpServer.h"
bool UdpServer::InitServer() {
    _socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if(_socket_fd < 0) {
        cerr << "socket fail" << endl;
        return false;
    }
    cout << "socket create success" << endl;
    return true;
}
UdpServer::~UdpServer() {
    if(_socket_fd > 0) close(_socket_fd);
}

当析构服务器时,可以将 _socket_fd 对应的文件进行关闭,但实际上不进行该操作也行,因为一般服务器运行后是就不会停下来的

#include "UdpServer.h"
int main() 
{
    UdpServer* server = new UdpServer;
    server->InitServer();
    return 0;
}

07774c9805b644d3955183b686be5af8.png


运行程序后套接字创建成功,对应获取到的文件描述符为3,因为0、1、2默认被标准输入流、标准输出流和标准错误流占用,此时最小的、未被利用的文件描述符就是3


3.1.2 服务端绑定

套接字已经创建成功了,但作为服务器来讲,若只是把套接字创建好了,那也只是在系统层面上打开了一个文件,操作系统将来并不知道是要将数据写入到磁盘还是刷到网卡,此时该文件还没有与网络关联起来。所以初始化服务器要做的第二件事就是绑定


bind函数


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

sockfd:绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符

addr:网络相关的属性信息,包括协议家族、IP地址、端口号等

addrlen:传入的addr结构体的长度

返回值:绑定成功返回0,绑定失败返回-1,同时错误码会被设置


struct sockaddr_in结构体


在绑定时需要将网络相关的属性信息填充到一个结构体当中,然后将该结构体作为bind函数的第二参数传入,就是 struct sockaddr_in 结构体


在 /usr/include/linux/in.h 中可以找到 struct sockaddr_in 的定义


7b8302e6aa10497d80103668486a49df.png


sin_family:表示协议家族

sin_port:表示端口号,是一个16位的整数

sin_addr:表示IP地址,是一个32位的整数

剩下的字段一般不做处理,当然也可以进行初始化


其中 sin_addr 的类型是 struct in_addr,实际该结构体中只有一个成员,该成员就是一个32位的整数,IP地址实际就存储在这个整数中


9514259cd63444d3a98c5f416797efeb.png


如何理解绑定?


在进行绑定的时候需要将IP地址和端口号告诉对应的网络文件,此时就可以改变网络文件当中文件操作函数的指向,将对应的操作函数改为对应网卡的操作方法,此时读数据和写数据对应的操作对象就是网卡了,所以绑定实际上就是将文件和网络关联起来


代码实现


由于绑定时需要用到IP地址和端口号,因此需要在服务器类中引入IP地址和端口号,在创建服务器对象时需要传入对应的IP地址和端口号,此时就可以根据传入的IP地址和端口号进行初始化

class UdpServer
{
public:
    UdpServer(uint16_t port,string ip):_socket_fd(-1),_port(port),_ip(ip) {}
    bool InitServer();
    ~UdpServer();
private:
    int _socket_fd;
    uint16_t _port;
    string _ip;
};

套接字创建完毕后就需要进行绑定,但在绑定之前我需先定义一个 struct sockaddr_in 结构变量,将对应的网络属性信息填充到该结构当中。由于该结构体中还有部分选填字段,因此最好在填充之前对该结构体变量里面的内容进行清空,然后再将协议家族、端口号、IP地址等信息填充到该结构体变量中


在发送到网络之前需要将端口号设置为网络序列,由于端口号是16位的,因此需要使用htons()函数将端口号转为网络序列。此外,由于网络当中传输的是整数IP,需要调用inet_addr()函数将字符串IP转换成整数IP(同时转成网络序列),然后再将转换后的整数IP进行设置


当网络属性信息填充完毕后,由于bind函数提供的是通用参数类型,因此在传入结构体地址时还需要将 struct sockaddr_in* 强转为 struct sockaddr* 类型后再进行传入

bool UdpServer::InitServer() {
    //创建套接字
    _socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if(_socket_fd < 0) {
        cerr << "socket fail" << endl;
        return false;
    }
    cout << "socket create success , fd:" << _socket_fd << endl;
    //填充网络通信相关信息
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(_port);
    local.sin_addr.s_addr = inet_addr(_ip.c_str());
    //绑定
    if(bind(_socket_fd, (struct sockaddr*)&local, sizeof(local)) < 0) {
        cerr << "bind fail" << endl;
        return false;
    }
    cout << "bind success" << endl;
    return true;
}


3.1.3 字符串IP VS 整数IP

网络传输数据时寸土寸金,若在网络传输时直接以基于字符串的点分十进制IP的形式进行IP地址的传送,那么此时一个IP地址就需要15个字节,但实际并不需要耗费这么多字节


IP地址实际可以划分为四个区域,其中每一个区域的取值都是0~255,而这个范围的数字只需要用8个bit位即可表示,因此只需要32个bit位就能表示一个IP地址。其中这个32位的整数的每一个字节对应的就是IP地址中的某个区域,将IP地址的这种表示方法称之为整数IP


d044fe60e3124ba280ff5235d8102a0e.png


采用整数IP的方案表示一个IP地址只需要4个字节,并且也能表示同样的含义,因此在网络通信时就没有用字符串IP而用的是整数IP,减少网络通信时数据的传送


字符串IP和整数IP相互转换


转换的方式有很多,比如可以定义一个位段A,位段A中有四个成员,每个成员的大小都是8个bit位,这四个成员就依次表示IP地址的四个区域,共32个bit位


然后再定义一个联合体IP,该联合体中有两个成员,其中一个是32位的整数,其代表的就是整数IP,还有一个就是位段A类型的成员,其代表的就是字符串IP

1bdbe0d45a6740498ea2c0c23a27c64b.png



由于联合体的空间是成员共享的,因此设置IP和读取IP的方式如下:


当想以整数IP的形式设置IP时,直接将其赋值给联合体的第一个成员即可

当想以字符串IP的形式设置IP时,先将字符串分成对应的四部分,然后将每部分转换成对应的二进制序列依次设置到联合体中第二个成员当中的p1、p2、p3和p4即可

当想取出整数IP时,直接读取联合体的第一个成员即可

当想取出字符串IP时,依次获取联合体中第二个成员当中的p1、p2、p3和p4,然后将每一部分转换成字符串后拼接到一起即可

注意: 在操作系统内部实际用的就是位段和枚举,来完成字符串IP和整数IP之间的相互转换的


inet_addr函数


将字符串IP转换成整数IP,转为网络序列

in_addr_t inet_addr(const char *cp);

inet_ntoa函数


将整数IP转换成字符串IP,转为主机序列

char *inet_ntoa(struct in_addr in);

传入inet_ntoa函数的参数类型是 in_addr ,因此在传参时不需要选中 in_addr 结构中的32位的成员传入,直接传入in_addr 结构体即可


3.2  启动运行服务端

UDP服务器的初始化就只需要创建套接字和绑定即可,当服务器初始化完毕后就可以启动服务器了


服务器实际上就是在周而复始的提供某种服务,服务器之所以称为服务器,是因为服务器运行起来后就永远不会退出,因此服务器实际执行的是死循环代码。由于UDP服务器是不面向连接的,因此只要UDP服务器启动后,就可以直接读取客户端发来的数据


recvfrom函数


读取数据

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地址、端口号等(输出型参数,但不可设置为nullptr、NULL)

addrlen:调用时传入期望读取的src_addr结构体的长度,返回时代表实际读取到的src_addr结构体的长度(输入输出型参数)

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


由于UDP是不面向连接的,因此除了获取到数据以外还需要获取到对端网络相关的属性信息,包括IP地址和端口号等

在调用recvfrom读取数据时,必须将addrlen设置为要读取的结构体对应的大小

由于recvfrom函数提供的参数也是struct sockaddr*类型的,因此在传入结构体地址时需要将struct sockaddr_in*类型进行强转

提供启动服务器接口


服务端通过recvfrom函数读取客户端数据,可以先将读取到的数据视为字符串,将读取到的数据的后一个位置设置为'\0',此时就可以将读取到的数据进行输出,同时也可以将获取到的客户端的IP地址和端口号也一并进行输出


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

void UdpServer::Start()
{
    char buffer[SIZE];
    while(true) 
    {
        struct sockaddr_in ping;
        socklen_t length =  sizeof(ping);
        ssize_t size = recvfrom(_socket_fd, buffer, SIZE - 1, 0, (struct sockaddr*)&ping, &length);
        if(size > 0) {
            buffer[size] = '\0';
            uint16_t port = ntohs(ping.sin_port);
            string ip = inet_ntoa(ping.sin_addr);
            cout << "[" << ip << ":" << port << "]#" << buffer << endl;
        }
        else {
            cerr << "recvfrom fail" << endl;
        }
    } 
}


注意:若调用recvfrom函数读取数据失败,可以打印一条提示信息,但不要让服务器退出,服务器不能因为读取某一个客户端的数据失败就退出


引入命令行参数


鉴于构造服务器时需要传入IP地址和端口号,可以引入命令行参数。此时当运行服务器时在后面跟上对应的IP地址和端口号即可


目前使用IP地址127.0.0.1。IP地址为127.0.0.1等价于localhost表示本地主机,被称为本地环回,数据只在本地协议栈中流动,不经过网络。先在本地测试能否正常通信,然后再进行网络通信测试

int main(int argc, char* argv[]) 
{
    if(argc < 3) {
        cerr << "Usage " << argv[0] << " ip port" << endl;
        return 1;
    }
    UdpServer* server = new UdpServer(string(argv[1]),atoi(argv[2]));
    server->InitServer();
    server->Start();
    return 0;
}

agrv数组里面存储的是字符指针,而端口号是一个整数,需要使用atoi函数将字符串转换成整数。然后就可以用这个IP地址和端口号来构造服务器了,服务器构造完成并初始化后就可以调用Start函数启动服务器了

4e36b5e9dfa24cf9ac5dc618e2f75faf.png



客户端代码还没有编写,可以使用 netstat命令 来查看当前网络的状态,这里可以选择nlup选项


-n:直接使用IP地址,而不通过域名服务器

-l:显示监控中的服务器的Socket

-t:显示TCP传输协议的连线状况

-u:显示UDP传输协议的连线状况

-p:显示正在使用Socket的程序识别码和程序名称

78c62ac0fdeb4ef884474a66878eb2d0.png


去掉 n选项,原本显示IP地址的位置变成了对应的域名服务器

5f75d41d10a9444889d6d82323c68ad9.png



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


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

目录
相关文章
|
2月前
|
开发者 Python
Python Socket编程:不只是基础,更有进阶秘籍,让你的网络应用飞起来!
在数字时代,网络应用成为连接世界的桥梁。Python凭借简洁的语法和丰富的库支持,成为开发高效网络应用的首选。本文通过实时聊天室案例,介绍Python Socket编程的基础与进阶技巧。基础篇涵盖服务器和客户端的建立与数据交换;进阶篇则探讨多线程与异步IO优化方案,助力提升应用性能。通过本案例,你将掌握Socket编程的核心技能,推动网络应用飞得更高、更远。
55 1
|
24天前
|
Kubernetes 网络协议 Python
Python网络编程:从Socket到Web应用
在信息时代,网络编程是软件开发的重要组成部分。Python作为多用途编程语言,提供了从Socket编程到Web应用开发的强大支持。本文将从基础的Socket编程入手,逐步深入到复杂的Web应用开发,涵盖Flask、Django等框架的应用,以及异步Web编程和微服务架构。通过本文,读者将全面了解Python在网络编程领域的应用。
21 1
|
27天前
|
Java
[Java]Socket套接字(网络编程入门)
本文介绍了基于Java Socket实现的一对一和多对多聊天模式。一对一模式通过Server和Client类实现简单的消息收发;多对多模式则通过Server类维护客户端集合,并使用多线程实现实时消息广播。文章旨在帮助读者理解Socket的基本原理和应用。
22 1
|
1月前
|
消息中间件 监控 网络协议
Python中的Socket魔法:如何利用socket模块构建强大的网络通信
本文介绍了Python的`socket`模块,讲解了其基本概念、语法和使用方法。通过简单的TCP服务器和客户端示例,展示了如何创建、绑定、监听、接受连接及发送/接收数据。进一步探讨了多用户聊天室的实现,并介绍了非阻塞IO和多路复用技术以提高并发处理能力。最后,讨论了`socket`模块在现代网络编程中的应用及其与其他通信方式的关系。
|
1月前
|
网络协议 Linux 应用服务中间件
Socket通信之网络协议基本原理
【10月更文挑战第10天】网络协议定义了机器间通信的标准格式,确保信息准确无损地传输。主要分为两种模型:OSI七层模型与TCP/IP模型。
|
2月前
|
网络协议 Python
网络世界的建筑师:Python Socket编程基础与进阶,构建你的网络帝国!
在数字宇宙中,网络如同复杂脉络连接每个角落,Python Socket编程则是开启这一世界的钥匙。本文将引导你从基础概念入手,逐步掌握Socket编程,并通过实战示例构建TCP/UDP服务器与客户端。你将学会使用Python的socket模块进行网络通信,了解TCP与UDP的区别,并运用多线程与异步IO提升服务器性能。跟随本文指引,成为网络世界的建筑师,构建自己的网络帝国。
36 2
|
2月前
|
网络协议 Python
告别网络编程迷雾!Python Socket编程基础与实战,让你秒变网络达人!
在网络编程的世界里,Socket编程是连接数据与服务的关键桥梁。对于初学者,这往往是最棘手的部分。本文将用Python带你轻松入门Socket编程,从创建TCP服务器与客户端的基础搭建,到处理并发连接的实战技巧,逐步揭开网络编程的神秘面纱。通过具体的代码示例,我们将掌握Socket的基本概念与操作,让你成为网络编程的高手。无论是简单的数据传输还是复杂的并发处理,Python都能助你一臂之力。希望这篇文章成为你网络编程旅程的良好开端。
59 3
|
2月前
|
网络协议 开发者 Python
网络编程小白秒变大咖!Python Socket基础与进阶教程,轻松上手无压力!
在网络技术飞速发展的今天,掌握网络编程已成为开发者的重要技能。本文以Python为工具,带你从Socket编程基础逐步深入至进阶领域。首先介绍Socket的概念及TCP/UDP协议,接着演示如何用Python创建、绑定、监听Socket,实现数据收发;最后通过构建简单的聊天服务器,巩固所学知识。让初学者也能迅速上手,成为网络编程高手。
74 1
|
2月前
|
网络协议 安全 网络安全
震惊!Python Socket竟能如此玩转网络通信,基础到进阶全攻略!
【9月更文挑战第12天】在网络通信中,Socket编程是连接不同应用与服务的基石。本文通过问答形式,从基础到进阶全面解析Python Socket编程。涵盖Socket的重要性、创建TCP服务器与客户端、处理并发连接及进阶话题如非阻塞Socket、IO多路复用等,帮助读者深入了解并掌握网络通信的核心技术。
66 6
|
1月前
|
网络协议 测试技术 网络安全
Python编程-Socket网络编程
Python编程-Socket网络编程
下一篇
无影云桌面