网络编程套接字(一)

网络编程套接字

  • 预备知识
    • 理解源IP地址和目的IP地址
    • 理解源MAC地址和目的MAC地址
    • 理解源端口号和目的端口号
    • PORT VS PID
    • 认识TCP协议和UDP协议
    • 网络字节序
  • socket编程接口
    • socket常见API
    • sockaddr结构
  • 简单的UDP网络程序
    • 服务端创建套接字
    • 服务端绑定
    • 运行服务器
    • 客户端代码编写
    • 本地测试
    • INADDR_ANY
  • 多线程情况下实现简易聊天室

预备知识

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

因特网上的每台计算机都有一个唯一的IP地址,如果一台主机上的数据要传输到另一台主机,那么对端主机的IP地址就应该作为该数据传输时的目的IP地址。但仅仅知道目的IP地址是不够的,当对端主机收到该数据后,对端主机还需要对该主机做出响应,因此对端主机也需要发送数据给该主机,此时对端主机就必须知道该主机的IP地址。因此一个传输的数据当中应该涵盖其源IP地址和目的IP地址,目的IP地址表明该数据传输的目的地,源IP地址作为对端主机响应时的目的IP地址。

在数据进行传输之前,会先自顶向下贯穿网络协议栈完成数据的封装,其中在网络层封装的IP报头当中就涵盖了源IP地址和目的IP地址。而除了源IP地址和目的IP地址之外,还有源MAC地址和目的MAC地址的概念。

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

大部分数据的传输都是跨局域网的,数据在传输过程中会经过若干个路由器,最终才能到达对端主机。
在这里插入图片描述
最开始的时候源MAC地址就是主机的MAC地址,目的MAC地址是路由器A的MAC地址,在经过路由器A以后,此时源MAC地址就是路由器A的MAC地址,目的MAC地址是路由器B的MAC地址,如此循环往复…,直到最后到达对端主机。

因此数据在传输的过程中是有两套地址:

  • 一套是源IP地址和目的IP地址,这两个地址在数据传输过程中基本是不会发生变化的(存在一些特殊情况,比如在数据传输过程中使用NET技术,其源IP地址会发生变化,但至少目的IP地址是不会变化的)。
  • 另一套就是源MAC地址和目的MAC地址,这两个地址是一直在发生变化的,因为在数据传输的过程中路由器不断在进行解包和重新封装。

理解源端口号和目的端口号

首先我们需要明确的是,两台主机之间通信的目的不仅仅是为了将数据发送给对端主机,而是为了访问对端主机上的某个服务。比如我们在用百度搜索引擎进行搜索时,不仅仅是想将我们的请求发送给对端服务器,而是想访问对端服务器上部署的百度相关的搜索服务。

socket通信的本质

通过IP地址和MAC地址我们已经可以向对端主机发送数据,但是我们所需要的不仅仅只是发送数据,而是访问对端主机上的某个服务进程,此时,数据的发送者也不再是主机,而是主机上的某个进程,比如当我们用浏览器访问数据时,实际就是浏览器进程向对端服务进程发起的请求。

在这里插入图片描述
socket通信本质上就是两个进程之间在进行通信,只不过这里是跨网络的进程间通信。比如逛淘宝和刷抖音的动作,实际就是手机上的淘宝进程和抖音进程在和对端服务器主机上的淘宝服务进程和抖音服务进程之间在进行通信。

因此进程间通信的方式除了管道、消息队列、信号量、共享内存等方式外,还有套接字,只不过前者是不跨网络的,而后者是跨网络的。

端口号

实际在两台主机上,可能会同时存在多个正在进行跨网络通信的进程,因此当数据到达对端主机后,必须要通过某种方法找到该主机上对应的服务进程,然后将数据交给该进程处理。而当该进程处理完数据后还要对发送端进行响应,因此对端主机也需要知道,是发送端上的哪一个进程向它发送的数据请求。

端口号(port)的作用实际就是标识一台主机上的一个进程。

  • 端口号是传输层协议的内容。
  • 端口号是一个2字节16位的整数。
  • 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理。
  • 一个端口号只能被一个进程占用。

IP地址能够唯一标识公网内的一台主机,而端口号能够唯一标识一台主机上的一个进程,因此用IP地址+端口号就能够唯一标识网络上的某一台主机的某一个进程。

当数据在传输层进行封装时,就会添加上对应源端口号和目的端口号的信息。这时通过源IP地址+源端口号就能够在网络上唯一标识发送数据的进程,通过目的IP地址+目的端口号就能够在网络上唯一标识接收数据的进程,此时就实现了跨网络的进程间通信。

端口号可以在两台不同的主机当中重复,但是在同一台主机上进行网络通信的进程的端口号不能重复。此外,一个进程可以绑定多个端口号,但是一个端口号不能被多个进程同时绑定。

PORT VS PID

进程IDPID)是用来标识系统内所有进程的唯一性的,它是属于系统级的概念;而端口号(port)是用来标识需要对外进行网络数据请求的进程的唯一性的,它是属于网络的概念。

一台机器上可能会有大量的进程,但并不是所有的进程都要进行网络通信,可能有很大一部分的进程是不需要进行网络通信的本地进程,此时PID虽然也可以标识这些网络进程的唯一性,但在该场景下就不太合适了。所以进行网络通信时为什么不直接用PID来代替port。

底层如何通过port找到对应进程的?

实际底层采用哈希的方式建立了端口号和进程PID或PCB之间的映射关系,当底层拿到端口号时就可以直接执行对应的哈希算法,然后就能够找到该端口号对应的进程。

认识TCP协议和UDP协议

网络协议栈是贯穿整个体系结构的,在应用层、操作系统层和驱动层各有一部分。当我们使用系统调用接口实现网络数据通信时,不得不面对的协议层就是传输层,而传输层最典型的两种协议就是TCP协议和UDP协议。

TCP协议

TCP协议叫做传输控制协议(Transmission Control Protocol),TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。如果两台主机之间想要进行数据传输,那么必须要先建立连接,当连接建立成功后才能进行数据传输。其次,TCP协议是保证可靠的协议,数据在传输过程中如果出现了丢包、乱序等情况,TCP协议都有对应的解决方法。

UDP协议

UDP协议叫做用户数据报协议(User Datagram Protocol),UDP协议是一种无需建立连接的、不可靠的、面向数据报的传输层通信协议。两台主机之间想要进行数据传输,那么直接将数据发送给对端主机就行了,但这也就意味着UDP协议是不可靠的,数据在传输过程中如果出现了丢包、乱序等情况,UDP协议本身是不知道的。

为什么UDP协议不可靠但却存在呢?

我们知道TCP作为一个可靠的协议,他的实现就会比较复杂,对于丢包,乱序等情况会有相应的处理办法;而UDP协议不可靠,实现就会比较简单,因为出现丢包和乱序他可能就不知道,能够快速传输数据,所以,我们对于一些有严格要求可靠性的数据,我们就使用TCP协议,而一些可以出现丢包和乱序的数据,我们就可以使用UDP协议。

网络字节序

网络中的大小端问题

  • 大端模式: 数据的高字节内容保存在内存的低地址处,数据的低字节内容保存在内存的高地址处。
  • 小端模式: 数据的高字节内容保存在内存的高地址处,数据的低字节内容保存在内存的低地址处。

对于同一台主机上,数据存储的方式一样,所以无论是大端存储还是小端存储,并不会出现问题,但是涉及到网络传输以后,在多台机器之间进行数据传输,可能一台机器存储方式为大端存储,另一台机器的存储方式为小端存储,在进行数据传输以后原本大端存储的是数据会被按小端方式识别出来,而原本小端存储的是数据会被按大端方式识别出来,导致数据识别出现错误;

所以网络当中传输的数据必须考虑大小端问题。因此TCP/IP协议规定,网络数据流采用大端字节序,即低地址高字节。无论是大端机还是小端机,都必须按照TCP/IP协议规定的网络字节序来发送和接收数据。

  • 如果发送端是小端,需要先将数据转成大端,然后再发送到网络当中。
  • 如果发送端是大端,则可以直接进行发送。
  • 如果接收端是小端,需要先将接收到数据转成大端后再进行数据识别。
  • 如果接收端是大端,则可以直接进行数据识别。

为什么网络字节序采用的是大端

该问题有很多不同说法,下面列举了两种说法:

  • 说法一:TCP在Unix时代就有了,以前Unix机器都是大端机,因此网络字节序也就采用的是大端,但之后人们发现用小端能简化硬件设计,所以现在主流的都是小端机,但协议已经不好改了。
  • 说法二: 大端序更符合现代人的读写习惯。

网络字节序与主机字节序之间的转换

为使网络程序具有可移植性,使同样的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位长整数从主机字节序转换为网络字节序。
  • 如果主机是小端字节序,则这些函数将参数做相应的大小端转换然后返回。
  • 如果主机是大端字节序,则这些函数不做任何转换,将参数原封不动地返回。

socket编程接口

socket常见API

创建套接字:(TCP/UDP,客户端+服务器)

int socket(int domain, int type, int protocol);

绑定端口号:(TCP/UDP,服务器)

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

监听套接字:(TCP,服务器)

int listen(int sockfd, int backlog);

接收请求:(TCP,服务器)

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

建立连接:(TCP,客户端)

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

sockaddr结构

套接字不仅仅支持网络间通信,还支持本地间通信,,提供了对于网络通信 sockaddr_in接口,对于本地通信则提供了sockaddr_un接口。

为了让套接字的网络通信和本地通信能够使用同一套函数接口,于是就出现了sockeaddr结构体,该结构体与sockaddr_insockaddr_un的结构都不相同,但这三个结构体头部的16个比特位都是一样的,这个字段叫做协议家族。
在这里插入图片描述
此时当我们在传递在传参时,就不用传入sockeaddr_insockeaddr_un这样的结构体,而统一传入sockeaddr这样的结构体。在设置参数时就可以通过设置协议家族这个字段,来表明我们是要进行网络通信还是本地通信,在这些API内部就可以提取sockeaddr结构头部的16位进行识别,进而得出我们是要进行网络通信还是本地通信,然后执行对应的操作。此时我们就通过通用sockaddr结构,将套接字网络通信和本地通信的参数类型进行了统一。

为什么没有用void代替struct sockaddr类型?

我们可以将这些函数的struct sockaddr参数类型改为void,此时在函数内部也可以直接指定提取头部的16个比特位进行识别,最终也能够判断是需要进行网络通信还是本地通信,那为什么还要设计出sockaddr这样的结构呢?

实际在设计这一套网络接口的时候C语言还不支持void*,于是就设计出了sockaddr这样的解决方案。并且在C语言支持了void*之后也没有将它改回来,因为这些接口是系统接口,系统接口是所有上层软件接口的基石,系统接口是不能轻易更改的,否则引发的后果是不可想的,这也就是为什么现在依旧保留sockaddr结构的原因。

简单的UDP网络程序

首先我们先包装一个日志文件,以便于更好的描述各种状态:

#pragma once#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>// 日志是有日志级别的
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4const char *gLevelMap[] = {"DEBUG","NORMAL","WARNING","ERROR","FATAL"
};#define LOGFILE "./threadpool.log"// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOWif(level== DEBUG) return;
#endifchar stdBuffer[1024]; //标准部分time_t timestamp = time(nullptr);snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);char logBuffer[1024]; //自定义部分va_list args;va_start(args, format);vsnprintf(logBuffer, sizeof logBuffer, format, args);va_end(args);printf("%s%s\n", stdBuffer, logBuffer);
}

服务端创建套接字

创建套接字我们需要调用socket函数,socket函数的原型如下:

int socket(int domain, int type, int protocol);

参数说明:

  • domain:创建套接字的域或者叫做协议家族,也就是创建套接字的类型。该参数就相当于cpp struct sockaddr结构的前16个位。如果是本地通信就设置为AF_UNIX,如果是网络通信就设置为AF_INET(IPv4)或AF_INET6(IPv6)。
  • type:创建套接字时所需的服务类型。其中最常见的服务类型是SOCK_STREAMSOCK_DGRAM,如果是基于UDP的网络通信,我们采用的就是SOCK_DGRAM,叫做用户数据报服务,如果是基于TCP的网络通信,我们采用的就是SOCK_STREAM,叫做流式套接字,提供的是流式服务。
  • protocol:创建套接字的协议类别。你可以指明为TCP或UDP,但该字段一般直接设置为0就可以了,设置为0表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议。

返回值说明:

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

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

网络协议栈是分层的,按照TCP/IP四层模型来说,自顶向下依次是应用层、传输层、网络层和数据链路层。而我们现在所写的代码都叫做用户级代码,也就是说我们是在应用层编写代码,因此我们调用的实际是下三层的接口,而传输层和网络层都是在操作系统内完成的,也就意味着我们在应用层调用的接口都叫做系统调用接口。

socket函数是被谁调用的?

socket这个函数是被程序调用的,但并不是被程序在编码上直接调用的,而是程序编码形成的可执行程序运行起来变成进程,当这个进程被CPU调度执行到socket函数时,然后才会执行创建套接字的代码,也就是说socket函数是被进程所调用的。

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

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

class UdpServer
{
public:UdpServer(uint16_t port, std::string ip = "") : _port(port), _ip(ip), _sock(-1){}bool initServer(){// 1.创建套接字_sock = socket(AF_INET, SOCK_DGRAM, 0);if (_sock < 0){logMessage(FATAL, "%d:%s", errno, strerror(errno));exit(1);}logMessage(NORMAL, "init UdpServer done....%s", strerror(errno));return true;}private:uint16_t _port;std::string _ip;int _sock;
};#endif

服务端绑定

套接字已经创建成功了,但作为一款服务器来讲,如果只是把套接字创建好了,那我们也只是在系统层面上打开了一个文件,操作系统将来并不知道是要将数据写入到磁盘还是刷到网卡,此时该文件还没有与网络关联起来。

由于现在编写的是不面向连接的UDP服务器,所以初始化服务器要做的第二件事就是绑定:

bind函数

绑定的函数叫做bind,该函数的函数原型如下:

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

参数说明:

  • sockfd:绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符。
  • addr:网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:传入的addr结构体的长度。

返回值说明:

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

struct sockaddr_in 结构体

/* 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)];};

通过查看struct sockaddr_in结构的定义,我们可以看到,struct sockaddr_in当中的成员如下:

  • sin_family:表示协议家族。
  • sin_port:表示端口号,是一个16位的整数。
  • sin_addr:表示IP地址,是一个32位的整数。

其中__SOCKADDR_COMMON就表示 sin_family,本质上就是一个宏定义:

#define	__SOCKADDR_COMMON(sa_prefix) \sa_family_t sa_prefix##family

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

/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr{in_addr_t s_addr;};

如何理解绑定?

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

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

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

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

class UdpServer
{
public:UdpServer(uint16_t port, std::string ip = "") : _port(port), _ip(ip), _sock(-1){}bool initServer(){// 1.创建套接字_sock = socket(AF_INET, SOCK_DGRAM, 0);if (_sock < 0){logMessage(FATAL, "%d:%s", errno, strerror(errno));exit(1);}// 2.bind将将用户设置的ip与port在内核中与我们当前进程强关联struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;//服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络local.sin_port = htons(_port);//先将点分十进制IP风格 -> 4字节//4字节主机序列 -> 网络序列local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());if(bind(_sock, (const sockaddr*)&local, sizeof(local)) < 0){logMessage(FATAL, "%d:%s", errno, strerror(errno));exit(2);}logMessage(NORMAL, "init UdpServer done....%s", strerror(errno));return true;}~UdpServer(){if(_sock >= 0)close(_sock);}private:uint16_t _port;std::string _ip;int _sock;
};

inet_addr函数

将字符串IP转换成整数IP的函数叫做inet_addr,该函数的函数原型如下:

in_addr_t inet_addr(const char *cp);

inet_ntoa函数

将整数IP转换成字符串IP的函数叫做inet_ntoa,该函数的函数原型如下:

char *inet_ntoa(struct in_addr in);

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

运行服务器

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

recvfrom函数

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地址、端口号等。
  • addrlen:调用时传入期望读取的src_addr结构体的长度,返回时代表实际读取到的src_addr结构体的长度,这是一个输入输出型参数。

返回值说明:

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

注意:

  • 由于UDP是不面向连接的,因此我们除了获取到数据以外还需要获取到对端网络相关的属性信息,包括IP地址和端口号等。
  • 在调用recvfrom读取数据时,必须将addrlen设置为你要读取的结构体对应的大小。
  • 由于recvfrom函数提供的参数也是struct sockaddr* 类型的,因此我们在传入结构体地址时需要将structsockaddr_in*类型进行强转。

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

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

	#define SIZE 1024void start(){char buffer[SIZE];for (;;){// 纯输出型参数struct sockaddr_in peer;bzero(&peer, sizeof(peer));// 输入: peer 缓冲区大小// 输出: 实际读到的peersocklen_t len = sizeof(peer);ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (s > 0){buffer[s] = 0;uint16_t cli_port = ntohs(peer.sin_port);      // 从网络中来std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节的网络序列的IP->本主机的字符串风格的IPprintf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);}// 分析处理数据,写回数据sendto(_sock, buffer, sizeof(buffer), 0, (const sockaddr *)&peer, len);}}

引入命令行参数

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

由于云服务器的原因,后面实际不需要传入IP地址,因此在运行服务器的时候我们只需要传入端口号即可,目前我们就手动将IP地址设置为127.0.0.1。IP地址为127.0.0.1实际上等价于localhost表示本地主机,我们将它称之为本地环回,相当于我们一会先在本地测试一下能否正常通信,然后再进行网络通信的测试。

#include <iostream>
#include "udp_server.hpp"static void usage(std::string proc)
{std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}
int main(int argc, char* argv[])
{if(argc != 2){usage(argv[0]);exit(3);}std::string ip = "127.0.0.1";uint16_t port = atoi(argv[1]);UdpServer* svr = new UdpServer(port, ip);svr->initServer();svr->start();return 0;
}

此时带上端口号运行程序就可以看到套接字创建成功、绑定成功,现在服务器就在等待客户端向它发送数据。
在这里插入图片描述
我们可以通过netstat命令来查看当前网络的状态,这里我们可以选择携带nlup选项。

netstat常用选项说明:

  • -n:直接使用IP地址,而不通过域名服务器。
  • -l:显示监控中的服务器的Socket。
  • -t:显示TCP传输协议的连线状况。
  • -u:显示UDP传输协议的连线状况。
  • -p:显示正在使用Socket的程序识别码和程序名称。

在这里插入图片描述

客户端代码编写

客户端其实跟服务端编代码编写差距并不太大,我们只需要按照服务端的套路进行编写就可以,最大的区别就是客户端需不需要考虑绑定问题。

关于客户端的绑定问题

对于网络通信来说,通信的双方都需要找到对方,所以客户端和服务端都需要对方的IP地址以及端口号,但是服务端就需要绑定,而客户端就不需要。

原因是服务器就是为别人服务的,就必须让别人知道IP地址以及端口号,所以服务端就必须是一个众所周知的端口号,选定后也就不能轻易发生改变,因此服务端就需要绑定端口号,绑定以后端口号才属于自己,一个端口号只能被一个进程所绑定,此时服务器也就独占这个端口号了。

虽然客户端也需要端口号,但是不需要绑定,因为如果客户端进行绑定,那么这个端口号就只能给此客户端使用,客户端不运行,端口号就无法给别人使用,别人使用了此端口号,客户端就无法运行,所以客户端就无需绑定端口号,只需要调用sendto这样的接口,操作系统就会自动分配一个端口号给客户端。

sendto函数

UDP客户端发送数据的函数叫做sendto,该函数的函数原型如下:

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

参数说明:

  • sockfd:对应操作的文件描述符。表示将数据写入该文件描述符索引的文件当中。
  • buf:待写入数据的存放位置。
  • len:期望写入数据的字节数。
  • flags:写入的方式。一般设置为0,表示阻塞写入。
  • dest_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:传入dest_addr结构体的长度。

返回值说明:

  • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

接下来我们就可以完成客户端代码编写,在这儿我们就不进行包装了:

#include <cstdio>
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <cerrno>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>static void usage(std::string proc)
{std::cout << "\nUsage: " << proc << " serverIp serverPort\n"<< std::endl;
}
int main(int argc, char *argv[])
{if (argc != 3){usage(argv[0]);exit(1);}int sock = socket(AF_INET, SOCK_DGRAM, 0);if(sock < 0){std::cerr << "socket" << std::endl;exit(2);}std::string message;struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(atoi(argv[2]));server.sin_addr.s_addr = inet_addr(argv[1]);char buffer[1024];while(true){std::cout << "请输入你的信息#:";std::getline(std::cin, message);if(message == "quit") break;// 当client首次发送消息给服务器的时候,OS会自动给client bind他的IP和PORTsendto(sock, message.c_str(), message.size(), 0, (const sockaddr*)&server, sizeof(server));struct sockaddr_in tmp;socklen_t len = sizeof(tmp);ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (sockaddr*)&tmp, &len);if(s > 0){buffer[s] = 0 ;std::cout << "server echo# " << buffer << std::endl;}}return 0;
}

本地测试

现在服务端和客户端的代码都已经编写完毕,我们可以先进行本地测试,此时服务器没有绑定外网,绑定的是本地环回。现在我们运行服务器时指明端口号为8080,再运行客户端,此时客户端要访问的服务器的IP地址就是本地环回127.0.0.1,服务端的端口号就是8080。户端运行之后提示我们进行输入,当我们在客户端输入数据后,客户端将数据发送给服务端,此时服务端再将收到的数据打印输出,这时我们在服务端的窗口也看到我们输入的内容。
在这里插入图片描述
此时使用netstat命令查看网络信息,可以看到服务端的端口是8080,客户端的端口是41634。这里客户端能被netstat命令查看到,说明客户端也已经动态绑定成功了,这就是我们所谓的网络通信。
在这里插入图片描述

INADDR_ANY

由于云服务器的IP地址是由对应的云厂商提供的,这个IP地址并不一定是真正的公网IP,这个IP地址是不能直接被绑定的,如果需要让外网访问,此时我们需要bind 0。系统当当中提供的一INADDR_ANY,这是一个宏值,它对应的值就是0。

如果我们不绑定INADDR_ANY,此时将服务端设置的本地环回改为我的公网IP,就会发现此时服务端就会绑定失败,因此如果我们需要让外网访问,那么在云服务器上进行绑定时就应该绑定INADDR_ANY,此时我们的服务器才能够被外网访问。

绑定INADDR_ANY的好处

当一个服务器的带宽足够大时,一台机器接收数据的能力就约束了这台机器的IO效率,因此一台服务器底层可能装有多张网卡,此时这台服务器就可能会有多个IP地址,但一台服务器上端口号为8080的服务只有一个。这台服务器在接收数据时,这里的多张网卡在底层实际都收到了数据,如果这些数据也都想访问端口号为8080的服务。此时如果服务端在绑定的时候是指明绑定的某一个IP地址,那么此时服务端在接收数据的时候就只能从绑定IP对应的网卡接收数据。而如果服务端绑定的是INADDR_ANY,那么只要是发送给端口号为8080的服务的数据,系统都会可以将数据自底向上交给该服务端。

在这里插入图片描述
此时当我们再用netstat命令查看时会发现,该服务器的本地IP地址变成了0.0.0.0,这就意味着该UDP服务器可以在本地读取任何一张网卡里面的数据。

多线程情况下实现简易聊天室

我们上述情况都是在单线程情况下进行讨论,接下来我们来看一下多线程情况下服务端与客户端通信,来实现一个见聊天室,首先,我们对线程进行封装:

#pragma once#include <iostream>
#include <pthread.h>
#include <string>
#include <functional>typedef void *(*func_t)(void *);class ThreadData
{
public:std::string name_;void *args_;
};class Thread
{
public:Thread(int num, func_t callback, void *args) : func_(callback){char nameBuffer[64];snprintf(nameBuffer, sizeof(nameBuffer), "Thread-%d", num);name_ = nameBuffer;tdata_.args_ = args;tdata_.name_ = name_;}void start(){pthread_create(&tid_, nullptr, func_, (void*)&tdata_);}std::string name(){return name_;}~Thread(){}private:std::string name_;func_t func_;ThreadData tdata_;pthread_t tid_;
};

此时服务端代码就更改为:

udp_server.hpp

#pragma once#ifndef _UDP_SEVER_HPP_
#define _UDP_SEVER_HPP_#define SIZE 1024#include "log.hpp"
#include <cstdio>
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <cerrno>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <unordered_map>
#include <queue>class UdpServer
{
public:UdpServer(uint16_t port, std::string ip = "") : _port(port), _ip(ip), _sock(-1){}bool initServer(){// 1.创建套接字_sock = socket(AF_INET, SOCK_DGRAM, 0);if (_sock < 0){logMessage(FATAL, "%d:%s", errno, strerror(errno));exit(1);}// 2.bind将将用户设置的ip与port在内核中与我们当前进程强关联struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;// 服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络local.sin_port = htons(_port);// 先将点分十进制IP风格 -> 4字节// 4字节主机序列 -> 网络序列local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());if (bind(_sock, (const sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "%d:%s", errno, strerror(errno));exit(2);}logMessage(NORMAL, "init UdpServer done....%s", strerror(errno));return true;}void start(){char buffer[SIZE];char key[64];for (;;){// 纯输出型参数struct sockaddr_in peer;bzero(&peer, sizeof(peer));// 输入: peer 缓冲区大小// 输出: 实际读到的peersocklen_t len = sizeof(peer);ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (s > 0){buffer[s] = 0;uint16_t cli_port = ntohs(peer.sin_port);      // 从网络中来std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节的网络序列的IP->本主机的字符串风格的IP// printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);snprintf(key, sizeof(key), "%s-%u", cli_ip.c_str(), cli_port);logMessage(NORMAL, "key:%s", key);auto it = _users.find(key);if(it == _users.end()){logMessage(NORMAL, "add new users:%s", key);_users.insert({key, peer});}}// 分析处理数据,写回数据//sendto(_sock, buffer, sizeof(buffer), 0, (const sockaddr *)&peer, len);for(auto iter : _users){std::string sendMessage = key;sendMessage += "#";sendMessage += buffer;logMessage(NORMAL, "push message to:%s", iter.first.c_str());sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (const sockaddr*)&iter.second, sizeof(iter.second));}}}~UdpServer(){if (_sock >= 0)close(_sock);}private:uint16_t _port;std::string _ip;int _sock;std::unordered_map<std::string, sockaddr_in> _users;
};#endif

udp_server.cc

此时创建我们的对象就可以采取智能指针的方式:

#include <iostream>
#include <memory>
#include "udp_server.hpp"static void usage(std::string proc)
{std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}
int main(int argc, char* argv[])
{if(argc != 2){usage(argv[0]);exit(3);}uint16_t port = atoi(argv[1]);std::unique_ptr<UdpServer> svr(new UdpServer(port));svr->initServer();svr->start();return 0;
}

udp_client.cc

#include <cstdio>
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <cerrno>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <memory>
#include "thrad.hpp"uint16_t serverport = 0;
std::string serverip;static void usage(std::string proc)
{std::cout << "\nUsage: " << proc << " serverIp serverPort\n"<< std::endl;
}static void *udpSend(void *args)
{int sock = *(int *)((ThreadData *)args)->args_;std::string name = ((ThreadData *)args)->name_;struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);server.sin_addr.s_addr = inet_addr(serverip.c_str());std::string message;while (true){std::cerr << "请输入信息#";std::getline(std::cin, message);if (message == "quit")break;sendto(sock, message.c_str(), message.size(), 0, (const sockaddr *)&server, sizeof(server));}return nullptr;
}static void *udpRcev(void *args)
{int sock = *(int *)((ThreadData *)args)->args_;std::string name = ((ThreadData *)args)->name_;char buffer[1024];while (true){memset(buffer, 0, sizeof(buffer));struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr *)&temp, &len);if (s > 0){buffer[s] = 0;std::cout << buffer << std::endl;}}
}
int main(int argc, char *argv[])
{if (argc != 3){usage(argv[0]);exit(1);}int sock = socket(AF_INET, SOCK_DGRAM, 0);if (sock < 0){std::cerr << "socket" << std::endl;exit(2);}serverport = atoi(argv[2]);serverip = argv[1];std::unique_ptr<Thread> sender(new Thread(1, udpSend, (void *)&sock));std::unique_ptr<Thread> recver(new Thread(2, udpRcev, (void *)&sock));sender->start();recver->start();sender->join();recver->join();close(sock);return 0;
}

最终我们运行我们的程序,此时我们打开3个客户端,分别输入内容,我们会发现每个客户端都可以接收到对方所发送的内容:
在这里插入图片描述
此时我们调用netstat命令查看,就会发现有3个服务端端口,而我们的客户端端口仅有8080一个,此时我们也就相当于搭建了一个简易的聊天室。
在这里插入图片描述

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

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

相关文章

02【保姆级】-GO语言开发注意事项(特色重点)

02【保姆级】-GO语言开发注意事项&#xff08;特色重点&#xff09; 一、Go语言的特性1.1 第一个hello word&#xff08;详解&#xff09;1.2 开发编译。&#xff08;重要点 / 面试题&#xff09;1.3 开发注意事项1.4 GO语言的转义字符1.5 注释1.6 API 文档 一、Go语言的特性 …

el-upload 组件上传/移除/报错/预览文件,预览图片、pdf 等功能

目录 页面代码样式代码页面展示 页面代码 dialog.vue <!-- 上传文件 --> <template><el-dialogtitle"上传文件":visible.sync"dialogVisible"width"60%"top"6vh":close-on-click-modal"false"close"h…

Python JSON 使用指南:解析和转换数据

JSON 是一种用于存储和交换数据的语法。JSON 是文本&#xff0c;使用 JavaScript 对象表示法编写。 Python 中的 JSON Python 有一个内置的 json 包&#xff0c;可用于处理 JSON 数据。 示例&#xff1a;导入 json 模块&#xff1a; import json解析 JSON - 从 JSON 转换为…

重温云栖,分享十年成长:我和云栖的故事

文章目录 前言活动背景我和云栖的交际历届峰会主题2009201020112012201320142015201620172018202120222023 技术带来的变化工作生活关注的领域 后记 前言 云栖大会&#xff0c;前身可追溯到2009年的地方网站峰会&#xff0c;2011年演变为阿里云开发者大会&#xff0c;2015年正式…

OpenCV标定演示,及如何生成标定板图片

标定的程序在官方的源码里有&#xff0c; opencv-4.5.5\samples\cpp\tutorial_code\calib3d\camera_calibration 很多小白不知道怎么跑起来&#xff0c;这个也怪OpenCV官方&#xff0c;工作没做完善&#xff0c;其实的default.xml是要自己手动改的&#xff0c;输入的图片也要…

在Maven中发布项目到Nexus私有服务器

一、测试环境 Sonatype Nexus 3.61.0-02 Maven 3.9.2 二、环境配置 2.1找到maven的配置文件 2.2添加私有仓库账户密码 <servers><server><id>nexus</id><username>admin</username><password>admin</password></server&…

rfsoc FPGA 49DR 16收16发模块

前面简单介绍过RFSOC板卡 https://blog.csdn.net/jingjiankai5228/article/details/114734631 整体来说RFSOC降低了传统AD DA软硬件开发难度&#xff0c;但是同样存在整数点FS/N谐波大的问题 交织采样是通过多个AD拼接完成的&#xff0c;所以校准比较关键&#xff0c;和以前常…

【React】03.脚手架的进阶应用

文章目录 暴露webpack配置暴露前后的区别config文件夹&#xff1a;scripts文件夹&#xff1a;package.json 常见的配置修改1.把sass改为less2.配置别名3.修改域名和端口号4.修改浏览器兼容5.处理Proxy跨域 2023年最新珠峰React全家桶【react基础-进阶-项目-源码-淘系-面试题】 …

【0基础学Java第五课】-- 方法的使用

5. 方法的使用 5.1 什么是方法5.2 方法定义5.3方法调用的执行过程例题&#xff1a;求n的阶乘和 5.4 实参和形参的关系&#xff08;重点&#xff09;5.5 没有返回值的方法5.6 方法重载5.7 方法签名5.8 递归5.9 递归练习按顺序打印一个数字的每一位(例如 1234 打印出 1 2 3 4)递归…

每天学习都很累,该怎么办?

中考淘汰一批人&#xff0c;高考又淘汰一批人&#xff0c;能杀进大学的&#xff0c;都知道高考的累。好不容易进了大学&#xff0c;却发现仍有打卡、作业、考试。 加上每天满满的课表&#xff0c;只是看看就让人心累。 为了奖学金或升学就业&#xff0c;又得去卷绩点、卷比赛、…

计算机毕业设计选题推荐-超市售货微信小程序/安卓APP-项目实战

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

操作系统:文件管理(一)文件系统基础

一战成硕 4.1 文件系统基础4.1.1 文件的基本概念4.1.2 文件控制块和索引结点4.1.3 文件的操作4.1.4 文件保护4.1.5 文件的逻辑结构4.1.6 文件的物理结构 4.1 文件系统基础 4.1.1 文件的基本概念 文件是以硬盘为载体的存储在计算机上的信息的结合。 在系统运行时&#xff0c;计…

深度神经网络的数学原理:基于超平面、半空间与线性区域的表示

概述 以前的文章主要描述了神经网络&#xff0c;即多层感知机、全连接模型的运行原理&#xff0c;还是以实验为主&#xff0c;数学描述为辅的方式&#xff0c;这篇文章以纯数学的视角来描述神经网络的运行原理&#xff0c;主要以前馈过程为主&#xff08;反向传播的动力学过程…

深度学习入门(二)之神经网络

文章目录 从感知机到神经网络神经网络的例子复习感知机激活函数 激活函数sigmoid函数阶跃函数的实现阶跃函数的图形sigmoid函数的图形sigmoid函数与阶跃函数比较非线性函数ReLU函数 多维数组的运算多维数组矩阵乘法神经网络的内积 三层神经网络的实现符号确认各层间信号传递的实…

【51单片机】LED与独立按键(学习笔记)

一、点亮一个LED 1、LED介绍 LED&#xff1a;发光二极管 补&#xff1a;电阻读数 102 > 10 00 1k 473 > 47 000 2、Keil的使用 1、新建工程&#xff1a;Project > New Project Ctrl Shift N &#xff1a;新建文件夹 2、选型号&#xff1a;Atmel-AT89C52 3、xxx…

uniapp原生插件之视频图片选择安卓原生插件

插件介绍 本地相册图片和视频多选Android扩展原生插件 插件地址 视频图片选择安卓原生插件 - DCloud 插件市场 超级福利 uniapp 插件购买超级福利 插件申请权限 存储卡读写权限 manifest.json权限列表 /* android打包配置 */"android" : {"permission…

Mozilla Firefox 119 现已可供下载

Mozilla Firefox 119 开源网络浏览器现在可以下载了&#xff0c;是时候先看看它的新功能和改进了。 Firefox 119 改进了 Firefox View 功能&#xff0c;现在可以提供更多内容&#xff0c;如最近关闭的标签页和浏览历史&#xff0c;你可以按日期或网站排序&#xff0c;还支持查…

spring 和 idea 建议不要使用 @Autowired注解

spring 和 idea 建议不要使用 Autowired注解 一. 问题描述二. 警告原因和如何去除三. 个人的收获和解决方案3. 1 个人感受3.2 通过构造函数解决警告问题 四. 小知识4.1 使用Autowired还会出现循环依赖的问题么4.2 Autowired 和 Resource区别 前言 这是我在这个网站整理的笔记,有…

vivo 自研蓝河操作系统 BlueOS 发布:支持大模型、BlueXlink 协议实现万物互联

大家好&#xff0c;我是 Lorin , 2023 年 11 月 1 日&#xff0c;在今天的 2023 年 vivo 开发者大会上&#xff0c;vivo 自主研发的蓝河操作系统&#xff08;BlueOS&#xff09;正式亮相。这款操作系统被宣传为一款面向未来的智能操作系统&#xff0c;具备出色的支持能力&#…

PlantSimulation访问本地Excel文件的方法

PlantSimulation访问本地Excel文件的方法 PlantSimulation访问本地Excel文件的方法PlantSimulation访问本地Excel文件的方法 //Param StatusTable,T_DataTable:object var T_DataTable:object:=DataTable IF NOT isComputerAccessPermittedMESSageBox("计算机访问被阻止,…