当前位置: 首页 > news >正文

socket网络编程

本文只介绍基于IPv4的socket网络编程

端口号

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

认识TCP协议

此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题.

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

认识UDP协议 

此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识, 后面再详细讨论.

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

网络字节序 

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

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

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

  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回
  • 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回

套接字编程种类

  1. 域间套接字编程---同一机器内
  2. 原始套接字编程---网络工具
  3. 网络套接字编程---用户间的网络通信

我们这里讲网络套接字编程

socket 常见API

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

Pv4地址用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结构体指针做为参数;

sockaddr 结构

sockaddr_in 结构 

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

in_addr结构

socket编程接口 

socket

创建套接字,返回值是网络文件描述符 (TCP/UDP, 客户端 + 服务器)

参数:

domain: 创建套接字的域,我们这里填AF_INET表示使用ipv4的网络协议

type:套接字对应的类型,SOCK_STREAM字节流套接子,SOCK_DGRAM数据报套接字,就是TCP协议是面向字节流的,UDP协议是面向数据报的

protocol:协议类型,我们这里不用理,设为0先

返回值,就是网络文件描述符

bind 

绑定套接字和端口号  (TCP/UDP, 服务器)

绑定的IP地址和端口号都在addr里,这个结构体需要我们自己设置,再传进去

伪代码:

struct sockaddr_in

{

        sin_family;//创建套接字的域  AF_INET

        sin_port;//端口号

        sin_addr;//IP地址

};

recvfrom

接收发给套接字的网络信息

参数很好理解,flags设为0,后面的结构体是输出型参数,记录是谁发的 

send

向套接字发送信息

地址转换函数

本文只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位的IP 地址但是我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示 和in_addr表示之间转换

头文件

#include <arpa/inet.h>

字符串转in_addr的函数

这个函数不仅把字符串转为32IP地址,还转为网络字节序了 

 in_addr转字符串的函数

简单的UDP网络程序

一个关于IP地址

当你使用的是云服务器,udp服务端绑定公网IP时,禁止绑定,因为我们看到的是虚拟的IP地址,一般绑定0.0.0.0的IP地址表示绑定任意地址的IP地址,凡是发给我服务端的数据,都要根据端口号向上交付。我们也不建议绑定固定地址,当

一个关于端口号port

端口号不是任意绑定的,[0 ,1023]:系统内定的端口号,一般都要有固定的应用层协议使用,比如http是80,https是443,mysql是3306,这个是例外。所以我们绑定端口号时,绑定1024+以上。

下面是udp服务端代码

  1. 创建套接字
  2. 绑定IP和端口号
  3. 接收消息
  4. 发送信息

udpserver.hpp

#pragma once#include <iostream>
#include <cstring>
#include <string>
#include <unordered_map>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"// typedef std::function<std::string(const std::string &, const std::string &, uint16_t)> func_t;std::string defaultip = "0.0.0.0";
uint16_t defaultport = 8888;
const int size = 1024;Log log;
enum
{SOCKET_ERR = 1,BIND_ERR,
};class UdpServer
{
public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip) : ip_(ip), port_(port), isrunning(false){}void Init(){// 1.创建udp socketsocketfd_ = socket(AF_INET, SOCK_DGRAM, 0);if (socketfd_ < 0){log(Fatal, "create socket erro,socketfd:%d", socketfd_);exit(SOCKET_ERR);}log(Info, "socket create success, socketfd: %d", socketfd_);// 2.绑定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必须是网络序列的if (bind(socketfd_, (struct sockaddr *)&local, sizeof(local)) < 0){log(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}log(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));}void ChectUser(const struct sockaddr_in &client, uint16_t clientport, const std::string &clientip){auto it = online_user_.find(clientip);if (it == online_user_.end()){online_user_.insert({clientip, client});std::cout << "[" << clientip << ":" << clientport << "]" << "add to online user" << std::endl;}}void BroadCast(const std::string &info, uint16_t clientport, const std::string &clientip){for (const auto &users : online_user_){std::string massege = "[";massege += clientip;massege += ":";massege += to_string(clientport);massege += "]# ";massege += info;socklen_t len = sizeof(users.second);sendto(socketfd_, massege.c_str(), massege.size(), 0, (struct sockaddr *)&users.second, len);}}void Run(){isrunning = true;char buff[size];while (true){memset(buff,0,size);struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(socketfd_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&client, &len);if (n < 0){log(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);std::string clientip = inet_ntoa(client.sin_addr);ChectUser(client, clientport, clientip);std::string info = buff;BroadCast(info, clientport, clientip);}}~UdpServer(){}private:int socketfd_;   // 网络文件描述符std::string ip_; // 绑定的IP地址uint16_t port_;  // 绑定的端口号bool isrunning;std::unordered_map<std::string, struct sockaddr_in> online_user_; // 存在线用户
};

main.cc    运行服务端

#include "udpserver.hpp"
#include <memory>
#include <cstdio>
#include <vector>
#include <functional>void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " port[1024+]\n"<< std::endl;
}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));UdpServer *svr = new UdpServer(port);svr->Init();svr->Run();return 0;
}

udpclient.cc   客户端

1. 创建套接字

2. client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!

 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!

其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以! 

系统什么时候给我bind呢?首次发送数据的时候

3.创建两个线程分别进行收发数据

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>void Usage(const std::string proc)
{std::cout << "\n\tUsage: " << proc << " serverip serverport\n"<< std::endl;
}struct ThreadData
{std::string serverip;struct sockaddr_in server;int sockfd;
};void *Recv_massege(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);char buff[1024];while (true){memset(buff, 0, sizeof(buff));struct sockaddr_in tmp;socklen_t len = sizeof(tmp);ssize_t n = recvfrom(td->sockfd, buff, sizeof(buff), 0, (struct sockaddr *)(&tmp), &len);if (n > 0){buff[n] = 0;std::cerr<< buff << std::endl;}}
}void *Send_massege(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);std::string massege;socklen_t len = sizeof(td->server);while (true){std::cout << "Please Enter@ ";getline(std::cin, massege);// 1. 数据 2. 给谁发sendto(td->sockfd, massege.c_str(), massege.size(), 0, (struct sockaddr *)&(td->server), len);}
}
// ./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]);ThreadData td;bzero(&td.server, sizeof(td.server));td.server.sin_addr.s_addr = inet_addr(serverip.c_str());td.server.sin_family = AF_INET;td.server.sin_port = htons(serverport);td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (td.sockfd < 0){std::cout << "sock erro" << std::endl;exit(1);}td.serverip = serverip;// client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!// 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!// 系统什么时候给我bind呢?首次发送数据的时候// 创建两个线程,分别收发数据pthread_t recv_tid, send_tid;pthread_create(&recv_tid, nullptr, Recv_massege, &td);pthread_create(&send_tid, nullptr, Send_massege, &td);pthread_join(recv_tid, nullptr);pthread_join(send_tid, nullptr);return 0;
}

相关文章:

  • MySQL高阶2004-职员招聘人数
  • 713. 乘积小于 K 的子数组 滑动窗口
  • Python Pandas数据处理效率提升指南
  • 【笔记】自动驾驶预测与决策规划_Part4_时空联合规划
  • 【GUI设计】基于Matlab的图像去噪GUI系统(8),matlab实现
  • 构建企业数字化转型的战略基石——TOGAF框架的深度解析
  • 物理学基础精解【26】
  • 录屏+GIF一键生成,2024年费软件大揭秘
  • Kubernetes 中 Pod 和 Node 的关系详解
  • 代码整洁之道 — 1 命名规范
  • C++——有3个学生,每个学生的数据包括:学号、姓名、3门课的成绩,从键盘输入三个学生的数据。要求打印学生三门课的平均分。
  • SpringBoot使用EasyPoi根据模板导出word or pdf
  • 什么是网络准入控制系统?2024年有哪些好用的网络准入控制系统?
  • 2024/10/1 操作系统大题专训之文件
  • SpringBoot实现社区医院数据集成解决方案
  • Apache的80端口被占用以及访问时报错403
  • flutter的key在widget list的作用以及必要性
  • Javascript基础之Array数组API
  • JS 面试题总结
  • Phpstorm怎样批量删除空行?
  • ReactNative开发常用的三方模块
  • Spring核心 Bean的高级装配
  • 浮动相关
  • 高性能JavaScript阅读简记(三)
  • 开放才能进步!Angular和Wijmo一起走过的日子
  • 聊聊directory traversal attack
  • 使用API自动生成工具优化前端工作流
  • 算法---两个栈实现一个队列
  • 我看到的前端
  • hi-nginx-1.3.4编译安装
  • k8s使用glusterfs实现动态持久化存储
  • postgresql行列转换函数
  • ​​​​​​​GitLab 之 GitLab-Runner 安装,配置与问题汇总
  • ​学习一下,什么是预包装食品?​
  • #php的pecl工具#
  • %3cscript放入php,跟bWAPP学WEB安全(PHP代码)--XSS跨站脚本攻击
  • ()、[]、{}、(())、[[]]等各种括号的使用
  • (6) 深入探索Python-Pandas库的核心数据结构:DataFrame全面解析
  • (6)设计一个TimeMap
  • (附源码)spring boot北京冬奥会志愿者报名系统 毕业设计 150947
  • (附源码)spring boot基于小程序酒店疫情系统 毕业设计 091931
  • (官网安装) 基于CentOS 7安装MangoDB和MangoDB Shell
  • (十)Flink Table API 和 SQL 基本概念
  • (一)硬件制作--从零开始自制linux掌上电脑(F1C200S) <嵌入式项目>
  • (转)负载均衡,回话保持,cookie
  • .jks文件(JAVA KeyStore)
  • .libPaths()设置包加载目录
  • .NET Core引入性能分析引导优化
  • .net 使用ajax控件后如何调用前端脚本
  • .NET和.COM和.CN域名区别
  • .NET周刊【7月第4期 2024-07-28】
  • /bin/bash^M: bad interpreter: No such file or directory
  • /tmp目录下出现system-private文件夹解决方法
  • @selector(..)警告提示
  • [ vulhub漏洞复现篇 ] struts2远程代码执行漏洞 S2-005 (CVE-2010-1870)