Linux网络编程套接字

目录

前言

一、预备知识

1.1 源IP地址和目的IP地址

1.2 区分端口号和进程ID

1.3 TCP协议和UDP协议

1.4 网络字节序

二、socket编程接口

2.1 socket套接字的概念

2.2 socket常见API

2.3 sockaddr结构

三、关于IP和Port的绑定问题

四、编写简单的UDP服务端和客户端


前言

        在进入本章内容学习前,我们先要了解网络编程是什么:

        实际上网路编程指的是网络上的主机通过不同的进程,以编程的方式实现网络通信。

一、预备知识

1.1 源IP地址和目的IP地址

        在IP数据包头部中有两个IP地址分别叫做IP地址目的IP地址源IP地址和目的IP地址在进行网络通讯时是不会改变的,你可以理解为外出旅行时的出发地和目的地在旅行过程中是不会改变的。

1.2 区分端口号和进程ID

进程PID已经能够标识一台主机上进程的唯一性了,为什么还要搞一个端口号?

  1. 不是所有的进程都要网络通信,但所有的进程都要有PID;
  2. 更重要的说应该是要实现系统和网络功能的解耦。        

        IP标定互联网中唯一一台主机,端口号port用来标识该主机上的唯一的一个进程,因此

IP地址+端口号(port)能够标识全网内唯一的一个进程。

        另外,一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定

        传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号。就是在描述 "数据是谁发的,要发给谁。

1.3 TCP协议和UDP协议

TCP协议(Transmission Control Protocol 传输控制协议):

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

UDP协议(User Datagram Protocol 用户数据报协议):

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

1.4 网络字节序

        我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?

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

        为使网络程序具有可移植性, 使同样的 C 代码在大端和小端计算机上编译后都能正常运行 , 可以调用以下库函数做网络字节序和主机字节序的转换:
  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数;
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送;
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

二、socket编程接口

2.1 socket套接字的概念

由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。
基于Socket套接字的网络程序开发就是网络编程。

套接字编程的种类:

  • 域间套接字编程(一般用于一个主机内的多个进程通信)
  • 原始套接字编程(通常是网络工具)
  • 网络套接字编程(用户间的网络通信)

2.2 socket常见API

// 创建 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);(头文件均为<sys/socket.h>)

 接收:

recvfrom() 函数用于从已连接或未连接的套接字接收数据,并将数据存储到缓冲区 buf 中。它的参数说明如下:

  • sockfd:套接字描述符。
  • buf:指向接收数据的缓冲区。
  • len:缓冲区的大小。
  • flags:额外的选项标志,通常设为 0。
  • src_addr:指向 struct sockaddr 结构体(见2.3)的指针,用于存储发送方的地址信息。
  • addrlen:指向 socklen_t 类型的指针,用于存储 src_addr 的长度。
  • 函数返回值为 ssize_t 类型,表示接收到的数据的字节数。如果返回 -1,则表示接收出错

发送:

sendto() 函数用于向指定的目标地址发送数据。它的参数说明如下:

  • sockfd:套接字描述符。
  • buf:指向要发送数据的缓冲区。
  • len:发送数据的大小。
  • flags:额外的选项标志,通常设为 0。
  • dest_addr:指向 struct sockaddr 结构体(见2.3)的指针,用于目标地址信息。
  • addrlen:指向 socklen_t 类型的指针,用于存储 src_addr 的长度。
  • 函数返回值为 ssize_t 类型,表示实际发送的数据的字节数。如果返回 -1,则表示接收出错

2.3 sockaddr结构

下面三种 sockaddr结构 分别对应:通用的网络地址结构体、IPv4地址结构体和Unix域地址结构体

接下来我们深入看看sockaddr_in的结构:

 框内第一个元素是端口号,第二个元素又是一个结构体类型:

可以看到它其实就是一个四字节的整数。

在使用时端口号是要在网络里来回发送的,因此必须保证端口号是网络字节序列;

同样的,IP如果要被网络使用,那么它也必须是网络字节序列的。

如果要把字符串IP转成四字节整型是不是很麻烦呢?

        其实系统早已为我们封装好了接口:

三、关于IP和Port的绑定问题

首先关于IP的绑定,若为0.0.0.0:

  1. 对于服务器:

    • 当服务器将IP地址绑定为 0.0.0.0 并且指定了特定的端口号时,服务器将会监听该端口号,并可以接受来自任何可用网络接口的连接请求。
    • 这意味着服务器将在所有网络接口上等待连接,包括本地回环接口(127.0.0.1)和物理网络接口。
    • 当有连接请求到达时,服务器可以根据需要选择特定的网络接口来处理连接。
  2. 对于客户端:

    • 当客户端将IP地址绑定为 0.0.0.0 并指定了特定的端口号时,客户端将通过任何可用的网络接口发送数据。
    • 这意味着客户端可以使用任何可用的网络接口来与目标服务器进行通信。

 关于Port的绑定:

        [0,1023]是系统内定的端口号,一般都有固定的应用层协议使用,如http:80 https:443

mysql:3306

四、编写简单的UDP服务端和客户端

UdpServer.hpp:

#pragma once#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"// using func_t = std::function<std::string(const std::string&)>;
typedef std::function<std::string(const std::string&)> func_t;Log lg;enum{SOCKET_ERR=1,BIND_ERR
};uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;class UdpServer{
public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0), port_(port), ip_(ip),isrunning_(false){}void Init(){// 1. 创建udp socketsockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // PF_INETif(sockfd_ < 0){lg(Fatal, "socket create error, sockfd: %d", sockfd_);exit(SOCKET_ERR);}lg(Info, "socket create success, sockfd: %d", sockfd_);// 2. bind socketstruct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port_); //需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的local.sin_addr.s_addr = inet_addr(ip_.c_str()); //1. string -> uint32_t 2. uint32_t必须是网络序列的 // ??// local.sin_addr.s_addr = htonl(INADDR_ANY);if(bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0){lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));}void Run(func_t func) // 对代码进行分层{isrunning_ = true;char inbuffer[size];while(isrunning_){struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);if(n < 0){lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}inbuffer[n] = 0;std::string info = inbuffer;std::string echo_string = func(info);sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (const sockaddr*)&client, len);}}~UdpServer(){if(sockfd_>0) close(sockfd_);}
private:int sockfd_;     // 网路文件描述符std::string ip_; // 任意地址bind 0uint16_t port_;  // 表明服务器进程的端口号bool isrunning_;
};

 UdpClient.cpp:

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>using namespace std;void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}// ./udpclient serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);struct sockaddr_in server;bzero(&server, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport); //?server.sin_addr.s_addr = inet_addr(serverip.c_str());socklen_t len = sizeof(server);int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){cout << "socker error" << endl;return 1;}// client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!// 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!// 系统什么时候给我bind呢?首次发送数据的时候string message;char buffer[1024];while (true){cout << "Please Enter@ ";getline(cin, message);// std::cout << message << std::endl;// 1. 数据 2. 给谁发sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, len);struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);if(s > 0){buffer[s] = 0;cout << buffer << endl;}}close(sockfd);return 0;
}

main.cpp:

#include "UdpServer.hpp"
#include <memory>
#include <cstdio>void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}std::string Handler(const std::string &str)
{std::string res = "Server get a message: ";res += str;std::cout << res << std::endl;return res;
}std::string ExcuteCommand(const std::string &cmd)
{// SafeCheck(cmd);FILE *fp = popen(cmd.c_str(), "r");if(nullptr == fp){perror("popen");return "error";}std::string result;char buffer[4096];while(true){char *ok = fgets(buffer, sizeof(buffer), fp);if(ok == nullptr) break;result += buffer;}pclose(fp);return result;
}// ./udpserver port
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpServer> svr(new UdpServer(port));svr->Init(/**/);svr->Run(Handler);return 0;
}
运行结果:

        

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/483746.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在VSCode中新配置一个ros项目

如何从零开始配置一个ros项目 预先准备初始化ros工程运行hello_ros进行第一个示例进行编译测试 预先准备 首先要在vscode中安装&#xff08;必须安装的&#xff09;&#xff1a;ros&#xff0c;c&#xff0c;cmake&#xff0c;cmake tools&#xff08;补全camkelist文件&#…

nginx优化配置

一 全局配置的六个模块简介 全局块&#xff1a;全局配置&#xff0c;对全局生效 events块&#xff1a;配置影响 Nginx 服务器与用户的网络连接 http块&#xff1a;配置代理&#xff0c;缓存&#xff0c;日志定义等绝大多数功能和第三方模块的配置 server块&#xff1a;配置…

交换瓶子【第七届】【省赛】【A组】

题目描述 有N个瓶子&#xff0c;编号 1 ~ N&#xff0c;放在架子上。 比如有5个瓶子&#xff1a; 2 1 3 5 4 要求每次拿起2个瓶子&#xff0c;交换它们的位置。 经过若干次后&#xff0c;使得瓶子的序号为&#xff1a; 1 2 3 4 5 对于这么简单的情况&#xff0c;显然&#…

Google插件Sider: ChatGPT Sidebar + GPTs GPT-4 Turbo Sider

Sider: ChatGPT Sidebar 可以使得满屏都是机器人&#xff0c;左侧栏可以打开访问GPT-4. 配置跳板机地址 google 搜索的右侧也有打开

ElasticSearch聚合操作

目录 ElasticSearch聚合操作 基本语法 聚合的分类 后续示例数据 Metric Aggregation Bucket Aggregation ES聚合分析不精准原因分析 提高聚合精确度 ElasticSearch聚合操作 Elasticsearch除搜索以外&#xff0c;提供了针对ES 数据进行统计分析的功能。聚合(aggregation…

Day32 文件属性 目录操作 库

文章目录 获取文件属性1.stat函数2.获取文件属性3.获取文件权限4.stat/fstat/lstat的区别&#xff1f; 目录操作库 Lib1.库的定义2.库的分类2.1 静态库2.2 动态库 3.静态库的制作4.动态库制作5.总结静态库和动态库 获取文件属性 1.stat函数 int stat(const char *path, struc…

C#,二叉搜索树(Binary Search Tree)的迭代方法与源代码

1 二叉搜索树 二叉搜索树&#xff08;BST&#xff0c;Binary Search Tree&#xff09;又称二叉查找树或二叉排序树。 一棵二叉搜索树是以二叉树来组织的&#xff0c;可以使用一个链表数据结构来表示&#xff0c;其中每一个结点就是一个对象。 一般地&#xff0c;除了key和位置…

uni-app 开发调试自动打开手机屏幕大小界面(Aidex移动端开发项目)

上效果&#xff1a; 下载Aidex的移动端项目并打开&#xff1a; 若依-ruoyi-AiDex-Uniapp: 若依-Ruoyi APP 移动解决方案&#xff0c;基于uniappuView封装的一套基础模版&#xff0c;开箱即用&#xff0c;免费开源&#xff0c;一份代码多终端适配&#xff0c;支持H5、支付宝小程…

Kotlin基础 7

1.apply函数详解 1.1. DSL /*** 为什么要传入扩展函数(泛型),而不是一个普通的匿名函数* T.()->Unit* 扩展函数里自带了接收者对象的this隐式调用* 为什么是泛型的扩展函数?* 因为是由this 隐式调用 this 类型就是泛型类型&#xff0c; 相当于this的扩展函数&#xff0c;…

Aidex移动端项目入门

运行效果 项目源码下载 若依-ruoyi-AiDex-Uniapp: 若依-Ruoyi APP 移动解决方案&#xff0c;基于uniappuView封装的一套基础模版&#xff0c;开箱即用&#xff0c;免费开源&#xff0c;一份代码多终端适配&#xff0c;支持H5、支付宝小程序、微信小程序、APP&#xff0c;实现了…

解决MobaXterm网络错误连接超时问题

报错页面&#xff1a; 报错原因&#xff1a; ①网络断开了 ②网络端口&#xff0c;端口号改变 解决办法&#xff1a; ①重新连接网络按R ②固定端口号 第一步&#xff1a;编辑------>虚拟机网络编辑器&#xff08;我的Linux在虚拟机里&#xff09; 第二步&#xff1a;用…

Chrome插件精选 — 缓存清理

Chrome实现同一功能的插件往往有多款产品&#xff0c;逐一去安装试用耗时又费力&#xff0c;在此为某一类型插件挑选出比较好用的一款或几款&#xff0c;尽量满足界面精致、功能齐全、设置选项丰富的使用要求&#xff0c;便于节省一个个去尝试的时间和精力。 1. Chrome清理大师…