欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > IT业 > 网络编程套接字(一)

网络编程套接字(一)

2024/10/27 16:16:04 来源:https://blog.csdn.net/m0_74824254/article/details/139546215  浏览:    关键词:网络编程套接字(一)

个人主页:Lei宝啊 

愿所有美好如期而遇


端口号

我们上网,就是两种动作:1.从远端拉取数据;2.将数据从本地推送到远端。             

我们是用户,平时怎么上网呢?打开抖音app,今日头条等等,将他们打开时,就是启动进程,也就是说,是进程在上网,是进程在从远端服务器上拉取数据到我们本地,我们才可以看到一个个的短视频,一篇篇文章。

我们是客户端,远端服务器是服务端,我们在请求数据时,将数据交给服务器并不是我们的目的,我们的目的是将数据交给服务器的指定服务进程,一个服务器并不止一个进程;同时,我们打开app,也可能不止启动一个进程,服务器将数据发送到我们主机上时,也需要确定要交给哪个进程,就像抖音服务器上拉取的数据,到我们主机上时,不可能交付给今日头条的进程,而是给我们抖音客户端的进程。

那么我们如何确定应该交付给哪个进程?

IP在互联网中唯一确定一台主机,而端口号唯一确定一台主机上的一个进程,所以,IP+port,我们就可以确定互联网中唯一的一个进程。

所以客户端可以确定互联网中唯一的一个进程,服务端也可以,他们之间通信时也就可以互相确定,也就可以唯一的找到彼此(src ip, src port,  dst ip, dst port)。

所以,网络通信的本质实际上就是进程间通信!进程间通信就需要使得进程之间看到同一份公共资源,所以网络通信,进程间看到的公共资源是什么?网络!

那么端口号用来标识一台主机上唯一的一个进程,但是我们在Linux系统部分,知道pid也是唯一标识一个进程的,那么为什么又出来一个端口号,不能直接用pid吗?有这样几个原因:如果使用pid,那么网络部分和系统部分将强耦合,如果将来系统部分不使用pid标识唯一进程,那么网络部分也得改;再一个,每一次进程重新启动时,pid都会改变,不够稳定。所以我们使用端口号,将进程PCB与其关联起来。

而通过ip地址和端口号进行通信,我们叫做socket网络通信。

简单认识TCP协议及UDP协议

这里我们先有一个直观认识:

TCP协议
  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流
UDP协议
  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报 

可靠传输和不可靠传输体现在,如果网络传输过程中丢包,那么TCP协议会补发,而UDP协议不会,但是这也就意味着UDP协议复杂性注定了低于TCP协议,我们这里对于他们的可靠性不要理解成优缺点,而是理解成特点。 

socket编程接口

UDP

创建socket文件描述符

其实这个接口就类似于创建一个文件,除了默认打开的三个文件,我们打开他,fd==3,就像是对方主机找到我们的主机,通过端口号找到我们的进程,之后通过进程打开的这个套接字文件进行网络通信。 


 绑定socket与网络信息

绑定sockfd与网络信息,网络信息就是struct sockaddr* 指向的结构体,这个结构体还有说法:

socket编程,是有不同种类的,有的是用来进行本地通信的,有的是用来进行跨网络通信的,还有的是用来进行网络管理的这样也就有不同的结构体类型,后来为了方便,就统一了接口,统一了类型,将不同种类的socket进行强制类型转换。

struct sockaddr,就是我们统一的结构体类型,struct sockaddr_in就是用于跨网络通信,struct sockaddr_un就是用于本地通信。

我们来看看他们内部的结构(本地通信这里不做介绍):

既然他们都有16位地址类型这个字段,也就是sa_family,那么sockaddr_in中的这个字段呢?我们看看第一个宏的定义:

也就是将名字进行拼接,传参为sin_,拼接##后的family就是sa_family,整体就是                  sa_family_t sa_family,所以强转以后,OS内部也就可以区分是要进行跨网络通信还是本地通信。

那么双方进行通信时,需要知道各自的ip地址和端口号,换句话说,要经过网络传输,但是,我客户端要怎么知道服务端的ip地址和端口号呢?

网络字节序与主机字节序

我们知道,内存中的多字节数据相对于内存地址有大端小端之分,那么不同的主机他们可能有的是大端,有的是小端,字节序不同的主机通过网络传输将数据传输过去,会出现无法识别的情况,所以也就有了规定,数据在进行网络传输时,需要先将数据转换为大端字节序,在解析数据时,也同样是如此,需要将数据网络数据的大端字节序转换为本主机的字节序。

这项工作需要我们去做吗?不需要:

库函数已经为我们做了这件事,他们的含义分别是:

  • 将无符号32位整数从主机序列转换为网络序列。
  • 将无符号16位整数从主机序列转换为网络序列。
  • 将无符号32位整数从网络序列转换为主机序列。
  • 将无符号16位整数从网络序列转换为主机序列。
代码编写(UDP)
服务端

服务端,我们的大致结构是这样的:

#include "server.hpp"
#include <memory>int main()
{unique_ptr<UdpServer> ptr = make_unique<UdpServer>(2020);ptr->Init();ptr->Start();return 0;
}

首先就是创建服务端,然后进行初始化,最后启动。8080是我们设置的服务端的端口号,端口号范围是0~65535,我们一般使用1024以后的端口号,测试时一般使用127.0.0.1 ip地址,我们称为本地环回,这样测试,数据不会真正通过网卡发送到网络中,而是在主机上走一圈再返回。 

如果是在云服务器上,那么还需要在云服务器的控制台将指定端口打开,否则是无法使用公网ip进行通信的。 

具体实现:

包含了线程池,线程,日志头文件,这些代码在以往的博客中都有,这里不再写出。

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <strings.h>
#include <cstdlib>
#include <unordered_map>
#include <list>
#include <arpa/inet.h>
#include <unistd.h>
#include "log.hpp"
#include "ThreadPool.hpp"
#include <string.h>
#include <queue>
#include "public.hpp"
using namespace std;class UdpServer
{
private:uint16_t _port; // 服务端端口号int _sockfd;unordered_map<string, uint16_t> usersinfo;queue<string> qs;public:UdpServer() {}UdpServer(uint16_t port) : _port(port){}~UdpServer(){}void Init(){_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // UDPif (_sockfd < 0){Log(Fatal, "socket create error");exit(SOCK_CREATE_ERROR);}// socket创建成功// 接下来需要将socket与网络信息进行绑定struct sockaddr_in server;bzero(&server, 0);server.sin_addr.s_addr = 0; // 表示绑定本机任意ip地址server.sin_family = AF_INET;server.sin_port = htons(_port);socklen_t len = sizeof(server);int bindval = bind(_sockfd, (struct sockaddr *)&server, len);if (bindval < 0){Log(Fatal, "socket bind error");exit(SOCK_BIND_ERROR);}// 绑定成功Log(Info, "socket init success");}void GetInfo(){char buffer[1024]; bzero(buffer, 0);struct sockaddr_in client;  bzero(&client, 0);socklen_t len = sizeof(client);while (true){           ssize_t realnum = recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&client, &len);if (realnum < 0){Log(Error, "get clientinfo error");continue;}Analyze ret(client);buffer[realnum] = '\0';cout << "[server message from: " << ret.getip() << "]: " << buffer << endl;qs.push(buffer);usersinfo[ret.getip()] = ret.getport();       }}void SendInfo(){struct sockaddr_in client;bzero(&client, 0);while (true){if(qs.empty()) continue;string str = qs.front();qs.pop();client.sin_family = AF_INET;socklen_t len = sizeof(client);for(auto &e: usersinfo){client.sin_port = htons(e.second);client.sin_addr.s_addr = inet_addr(e.first.c_str());ssize_t val = sendto(_sockfd, str.c_str(), str.size(), 0, (struct sockaddr *)&client, len);}}}// 在这里要实现多线程,使用线程池,在全局预先创建好线程池// 服务端接受和发送消息方法分开写void Start(){ThreadPool<function<void()>> *Pool = ThreadPool<function<void()>>::GetPool();Pool->ThreadInit();Pool->Start();function<void()> func1 = bind(&UdpServer::GetInfo, this);Pool->Push(func1);function<void()> func2 = bind(&UdpServer::SendInfo, this);Pool->Push(func2);Pool->Wait();}
};
客户端

客户端就比较简单一点

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <strings.h>
#include <cstdlib>
#include <unordered_map>
#include <list>
#include <string>
#include <arpa/inet.h>
#include "log.hpp"
#include "ThreadPool.hpp"
#include <string.h>
#include "public.hpp"
using namespace std;class UdpClient
{
private:string _ip;uint16_t _port;int _sockfd;public:UdpClient() {}UdpClient(string ip, uint16_t port) : _ip(ip), _port(port){}void Init(){_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // UDPif (_sockfd < 0){Log(Fatal, "socket create error");exit(SOCK_CREATE_ERROR);}// socket创建成功, 客户端初始化完成// 客户端不需要进行绑定,在向服务端发送消息时OS自动完成}void GetInfo(){char buffer[1024]; bzero(buffer, 0);  struct sockaddr_in server; bzero(&server, 0);socklen_t len = sizeof(server);while (true){               ssize_t realnum = recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&server, &len);if (realnum > 0){Analyze ret(server);buffer[realnum] = '\0';cout << "[client message from: " << ret.getip() << "]: " << buffer << endl;}    }}void SendInfo(){struct sockaddr_in goal;bzero(&goal, 0);goal.sin_family = AF_INET;goal.sin_addr.s_addr = inet_addr(_ip.c_str());goal.sin_port = htons(_port); // port主机序列转网络序列string str;while (true){cout << "Please Enter# ";getline(cin, str);// 将消息发送给服务端ssize_t val = sendto(_sockfd, str.c_str(), str.size(), 0, (struct sockaddr *)&goal, sizeof(goal));if (val < 0){Log(Error, "send clientinfo error");perror("sendto:");continue;}}}void Start(){ThreadPool<function<void()>> *Pool = ThreadPool<function<void()>>::GetPool();Pool->ThreadInit();Pool->Start();function<void()> func1 = bind(&UdpClient::GetInfo, this);Pool->Push(func1);function<void()> func2 = bind(&UdpClient::SendInfo, this);Pool->Push(func2);Pool->Wait();}
};

之后就可以运行了,服务端直接./server,客户端./client ip port即可。


如果有需要所有文件,可以后台私信我,看到会回。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com