目录
[2、Socket 的地址结构和一系列转换函数](#2、Socket 的地址结构和一系列转换函数)
[(2)三种 Socket 地址结构体](#(2)三种 Socket 地址结构体)
[(1)sockaddr 结构](#(1)sockaddr 结构)
[(2)sockaddr_in 结构](#(2)sockaddr_in 结构)
[(3)in_addr 结构](#(3)in_addr 结构)
[1. UDP 的 C/S 网络通信模型](#1. UDP 的 C/S 网络通信模型)
[(1)UDP 服务器通信流程](#(1)UDP 服务器通信流程)
[(2)UDP 客户端通信流程](#(2)UDP 客户端通信流程)
[(3)UDP 服务器、客户端通信流程图](#(3)UDP 服务器、客户端通信流程图)
[2、UDP 的 C/S 网络通信实现](#2、UDP 的 C/S 网络通信实现)
[绑定 bind](#绑定 bind)
[读取数据 recvfrom](#读取数据 recvfrom)
[UDP(Windows 环境下 C++ 实现)](#UDP(Windows 环境下 C++ 实现))
一、预备知识
1、IP地址
因特网是在网络级进行互联的,因此,因特网在网络层(IP 层)完成地址的统一工作,把不同物理网络的地址统一到具有全球惟一性的 IP地址上,IP 层所用到的地址叫作因特网地址,又叫 IP 地址。IP 地址的意义就是标识公网内唯一一台主机。
在 IP 数据包头部中 有两个 IP 地址, 分别叫做源 IP 地址 和目的 IP 地址。
如果我们的台式机或者笔记本没有 IP 地址就无法上网,而因为每台主机都有 IP 地址,所以注定了数据从一台主机传输到另一台主机就一定有源 IP 地址和目的 IP 地址,所以在报头中就会包含源IP 地址和目的 IP 地址。
2、端口号
网络通信的本质是进程间通信,有了 IP 就可以标识公网内唯一的一台主机,想要完成网络通信我们还需要一个东西来标识一台主机上的某个进程,这个标识就是端口号(port)。
端口号是传输层协议的内容,它包括如下几个特点:
- 端口号是一个 2 字节,16 比特位的整数。
- 一台主机中,一个端口号只能被一个进程所占用。
IP 地址(标识唯一主机)+ 端口号(标识唯一进程)能够标识网络上的某一台主机的某一个进程(全网唯一的进程)
端口号的解释:
- HTTP 通信使用的端口号是 80。在浏览器中输入网址并访问一个网站时,浏览器会与服务器进行 HTTP 通信。在这个过程中,浏览器将通过端口号 80 发送请求,以与服务器上运行的 Web 服务器进行通信。Web 服务器接收到请求后,会将相应的网页内容返回给浏览器,并通过端口号 80 将响应发送回浏览器。因此,端口号 80 在这种情况下用于标识 HTTP 通信。
- FTP 通信使用的端口号是 21。 使用 FTP 客户端与远程服务器进行文件传输时,通常使用的端口号是 21。FTP 客户端通过端口号 21 与 FTP 服务器建立连接并发送指令来上传、下载或删除文件。端口号 21 被 FTP 协议保留,用于标识 FTP 通信。
每个端口号都有特定的作用和用途,例如常见的端口号有:
- 20 和 21:FTP
- 22:SSH
- 25:SMTP(用于发送电子邮件)
- 53:DNS(域名系统)
- 80:HTTP
- 443:HTTPS
既然 pid 已经做到唯一标识一个进程,为何还要引入端口号呢?
我们可以从生活的角度去理解这种情况:即然每个人都有了唯一标识自己的身份照号,为何学校还要给我们分配学号呢?直接用身份照号不行吗?
在学校我们用学号,相比于身份证更简便,假如我的学号是2211211023,这样就能看到我是22级的,方便阅读信息。可是出了学校,别人并不能通过学号辨别你。也许你在不同的学习有不同的学号。pid和端口号也是一样的。
- 首先 pid 是系统规定的,而 port 是网络规定的,这样就可以把系统和网络解耦。
- port 标识服务器的唯一性不能做任何改变,要让客户端能找到服务器,就像 110,120 一样不能被改变,而 pid 每次启动进程,pid 就会改变。
- 不是所有的进程都需要提供网络服务或请求(不需要 port),但每个进程都需要 pid。
虽然一个端口号只能绑定一个进程,但是一个进程可以绑定多个端口号。前面说了有源 IP 和目的 IP,而这里的 port 也有源端口号和目的端口号。我们在发送数据的时候也要把自己的 IP 和端口号发送过去,因为数据还要被发送回来,所以发送数据时一定会多出一部分数据(以协议的形式呈现)。
3、Socket网络通信
socket 通信的本质就是跨网络的进程间通信,任何的网络客户端和网络服务如果要进行正常的数据通信,它们必须要有自己的端口号和匹配所属主机的 IP 地址。
4、认识TCP/UDP协议
我们进行网络编程时通常是在应用层编码,应用层下面就是传输层。应用层往下传输数据时不必担心也没有必要知道数据的传输情况如何,这个具体地交给传输层来解决,所以我们有必要简单了解一下传输层的两个重要协议 TCP 和 UDP。
(1)TCP协议
TCP (Transmission Control Protocol 传输控制协议)的特点:
- 传输层协议
- 有连接(在正式通信前要先建立连接)
- 可靠传输(在内部帮我们做可靠传输工作)
- 面向字节流
(2)UDP协议
UDP 全称 User Datagram Protocol,即用户数据报协议,它有如下特点:
- 属于传输层协议
- 无连接
- 不可靠传输
- 面向数据报
在我们的认知里一定是安全、稳定的才好,那传输层为什么还要引入一个不可靠传输方式的 UDP 协议呢?TCP 协议虽然是可靠传输,但是"可靠"是要付出一些效率上的代价的,可能会导致传输速度比较慢,而且实现起来相对复杂;以这个角度去看 UDP 协议,虽然可能在传输过程中出现丢包的情况,但效率上是要比 TCP 更快的。通常两个协议我们可以搭配起来使用,网速快时用 TCP 协议,网速慢时用 UDP 协议,但如果是要传输重要数据的话就应该用 TCP 了。
(3)网络字节序
我们知道,内存中的数据权值排列相对于内存地址的大小有大端和小端之分:
- 小端:低权值的数放入低地址。
- 大端:低权值的数放入高地址。
数据在发送时,发送主机通常将发送缓冲区中的数据按内存地址从低到高 的顺序以字节为单位发出接收主机把接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序以字节为单位保存的。即先发出低地址的数据,后发出高地址的数据;接收到的数据也是按低地址到高地址的顺序接收。
如果发送端和接收端主机的存储字节序不同,则会造成发送的数据和识别出来的数据不一致的问题,如下图所示:
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。TCP/IP 协议规定:网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机,都会按照这个 TCP/IP 规定的网络字节序来发送/接收数据。如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可。

为使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
- h 表示 host,n 表示 network,l 表示 32 位长整数,s 表示 16 位短整数。
- 例如 htonl 表示将 32 位的长整数从主机字节序转换为网络字节序,例如将 IP 地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
二、socket网络套接字
1、概念
socket 通常也称为"套接字",程序可以通过"套接字"向网络发出请求或者响应网络请求。socket 位于传输层之上、应用层之下。socket 编程是通过一系列系统调用完成应用层协议,如 FTP、Telent、HTTP 等应用层协议都是通过 socket 编程来实现的。
从套接字所处的位置来讲,套接字上连应用进程,下接网络协议栈,是应用程序与网络协议栈进行交互的接口。

套接字可以看作是通信的两个端点,一个是服务器端的套接字,另一个是客户端的套接字。通过套接字,服务器端和客户端可以相互发送和接收数据。
在网络通信中,套接字使用网络协议(如 TCP/IP、UDP 等)来完成数据的传输和通信。根据所使用的网络协议的不同,套接字可以分为两种类型:
- 流套接字(Stream Socket,也称为面向连接的套接字):基于 TCP 协议,提供可靠的、面向连接的通信。使用流套接字时,数据可以按照发送的顺序和完整性进行传输,确保数据的准确性。流套接字的通信方式类似于电话通信,需要在通信前先建立连接。
- 数据报套接字(Datagram Socket,也称为无连接的套接字):基于 UDP 协议,提供不可靠的、无连接的通信。使用数据报套接字时,数据以数据包的形式进行传输,不保证数据的顺序和完整性。数据报套接字适用于一次性发送不需要可靠传输的数据。
Linux 和 UNIX 的 I/O 内涵是系统中的一切都是文件。当程序在执行任何形式的 I/O 时,程序都是在读或者在写一个文件描述符,从而实现操作文件,但是,这个文件可能是一个 socket 网络连接、目录、FIFO、管道、终端、外设、磁盘上的文件。一样的道理,socket 也是使用标准 Linux 文件描述符和其他网络进程进行通信的。
socket 函数基本为系统调用函数,它是操作系统向网络通信进程提供的函数接口。
在TCP/IP协议中, 用 "源IP", "源端口号", "目的IP", "目的端口号", "协议号" 这样一个五元组来标识一个网络通信,我们可以用 netstat -n 命令查看当前主机下已经建立链接的网络通信
IP地址、端口号、socket 套接字三者在数据结构上的联系

2、Socket 的地址结构和一系列转换函数
(1)socket常见的API
socket
创建 socket 文件描述符(TCP/UDP, 客户端 + 服务器)


bind
绑定端口号( TCP/UDP, 服务器)
listen
开始监听 socket(TCP, 服务器)

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

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

(2)三种 Socket 地址结构体
socket API 是一层抽象的网络编程接口,适用于各种底层网络协议,如:IPv4、IPv6,以及后面要讲的 UNIX Domain Socket。然而,各种网络协议的地址格式并不相同。
套接字有不少类型,常见的有三种:
- 原始 socket
- 域间 socket
- 网络 socket
三种应用场景:网络套接字主要运用于跨主机之间的通信,也能支持本地通信,而域间套接字只能在本地通信,而原始套接字可以跨过传输层(TCP/IP 协议)访问底层的数据。
为了方便,设计者只使用了一套接口,这样就可以通过不同的参数来解决所有的通信场景。这里举两个具体的套接字类型:sockaddr_in 和 sockaddr_un:

可以看到 sockaddr_in 和 sockaddr_un 是两个不同的通信场景,区分它们就用 16 地址类型协议家族的标识符。但是,这两个结构体都不用,我们用 sockaddr。
比方说我们想用网络通信,虽然参数是 const struct sockaddr *addr,但实际传递进去的却是 sockaddr_in 结构体(注意要强制类型转换)。在函数内部一视同仁,全部看成 sockaddr 类型,然后根据前两个字节判断到底是什么通信类型然后再强转回去。可以把 sockaddr 看成基类,把 sockaddr_in 和 sockaddr_un 看成派生类,构成了多态体系。
- IPv4 和 IPv6 的地址格式定义在 netinet/in.h 中,IPv4 地址用 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 结构体指针做为参数
(1)sockaddr 结构

(2)sockaddr_in 结构

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

in_addr 用来表示一个 IPv4 的 IP 地址,其实就是一个 32 位的整数。
(3)IP地址转换函数
IP 地址转换函数是指完成点分十进制数 IP 地址(是一个字符串)与二进制数IP地址之间的相互转换。IP 地址转换主要由 inet_aton、inet_addr 和 inet_ntoa 这三个函数完成,但它们都只能处理 IPv4 地址,而不能处理 IPv6 地址。这三个函数的函数原型及其具体说明如下。
1、inet_addr
2、inet_aton
3、inet_ntoa
三、UDP套接字编程
UDP 协议是非连接非可靠的数据传输,常用在对数据质量要求不高的场合。UDP 服务器通常是非连接的,因而,UDP 服务器进程不需要像 TCP 服务器那样在监听套接字上接收新建的连接;UDP 只需要在绑定的端口上等待客户机发送过来的 UDP 数据报文,并对其进行处理和响应。
一个 TCP 服务进程只有在完成了对某客户机的服务后,才能为其它的客户机提供服务。而 UDP 服务器只是接收数据报文,处理并返回结果。UDP 支持广播和多播,如果要使用广播和多播,必须使用 UDP 套接字。UDP 套接字没有连接的建立和终止过程,UDP 只需要两个分组来交换一个请求和答应。UDP 不适合海量数据的传输。
1. UDP 的 C/S 网络通信模型
(1)UDP 服务器通信流程
- 建立 UDP 套接字
- 绑定套接字到特定的地址
- 等待并接受客户端信息
- 处理客户端请求
- 发送信息给客户端
- 关闭套接字
(2)UDP 客户端通信流程
- 建立 UDP 套接字
- 发送信息给服务器
- 接收来自服务器的信息
- 关闭套接字
(3)UDP 服务器、客户端通信流程图

2、UDP 的 C/S 网络通信实现
UDP服务端基本框架
udp_server.cpp
- 创建一个服务端对象
- 初始化服务端对象
- 启动服务端对象

(1)UDP服务端的初始化
创建套接字
在通信之前要先把网卡文件打开,函数作用:打开一个文件,把文件和网卡关联起来

- domain:是一个域,标识了这个套接字的通信类型(网络或者本地)。

第一个 AF_UNIX 表示本地通信,而 AF_INET 表示网络通信
type:套接字提供服务的类型。

这里我们讲的是 UDP,所以使用 SOCK_DGRAM。
protocol:想使用的协议,默认为 0 即可。因为前面的两个参数就已经决定了是 TCP 还是 UDP 协议了。
返回值,成功返回一个新的套接字描述费用,失败返回-1错误码被设置。

接下来我们创建套接字,创建完套接字我们要bind绑定

绑定 bind

所以我们要先定义一个 sockaddr_in 结构体填充数据,再传递进去。
点分十进制字符串风格的 IP 地址(例:"192.168.110.132" )每一个区域取值范围是 [0-255]:1字节 -> 4个区域。理论上,表示一个IP地址,其实4字节就够了。点分十进制字符串风格的 IP 地址为4字节。
返回值,成功0被返回,失败-1被返回,错误码被设置


这几个参数是什么呢?
cpp
struct sockaddr_in {
short int sin_family; // 地址族,一般为AF_INET或PF_INET
unsigned short int sin_port; // 端口号,网络字节序
struct in_addr sin_addr; // IP地址
unsigned char sin_zero[8]; // 用于填充,使sizeof(sockaddr_in)等于16
};
创建结构体后要先清空数据(初始化),我们可以用 memset,系统也提供了接口:


填充端口号的时候要注意端口号是两个字节的数据 ,涉及到大小端问题。
接口我们在网络字节序具体介绍了

对于 IP,首先我们要先转成整数,再解决大小端问题。系统给了直接能解决这两个问题的接口


为什么这里的IP为什么是local.sin_addr.s_addr

这里镶嵌了一个结构体

进行绑定bind

(2)启动服务端
作为一款网络服务器,是永远不退出的。
服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了
首先要知道服务器要死循环,永远不退出,除非用户删除。站在操作系统的角度,服务器是常驻内存中的进程,而我们启动服务器的时候要传递进去 IP 和端口号。
我们要要进行网络通信,在网络基础1的时候给大家讲了,报头包含了对方的IP和端口号还包含了自己的IP和端口号。
读取数据 recvfrom

- sockfd:从哪个套接字读。
- buf:数据放入的缓冲区。
- len:缓冲区长度。
- flags:读取方式。 0 代表阻塞式读取。
- src_addr 和 addrlen:输出型参数,返回对应的消息内容是从哪一个客户端发出的。第一个是自己定义的结构体,第二个是结构体长度。
返回值


现在我们想要知道是谁发送过来的消息,信息都被保存到了 client结构体中,我们知道 IP 信息在 client.sin_addr.s_addr 中。首先这是一个网络序列,要转成主机序列,其次为了方便观察,要把它转换成点分十进制 。
操作系统给了一个接口能够解决这两个问题:

inet_ntoa 这个函数返回了一个 char*,很显然是这个函数自己在内部为我们申请了一块内存来保存 ip 的结果。那么是否需要调用者手动释放呢?

man 手册上说,inet_ntoa 函数是把这个返回结果放到了静态存储区。这个时候不需要我们手动进行释放。
同样获取端口号的时候也要由网络序列转成主机序列:

4字节的网络序列要转化回本主机的字符串风格的IP


我们收到了消息,把数据发回
发回消息sendto

- sockfd:套接字文件描述符
- buf:发送数据空间首地址
- len:发送数据长度
- flags:默认为0
- dest_addr:目的IP地址存放空间首地址
- addrlen:目的地址的长度

完整代码
UdpServe.hpp
cpp
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <string.h>
#include <cerrno>
#include "log.hpp"
Log lg;
const std::string defaultip = "0.0.0.0";
enum
{
SockfdErr = 1,
BindErr,
};
class UdpServer
{
public:
UdpServer(const uint16_t& port = 8080,const std::string& ip = defaultip)
:_port(port)
,_ip(ip)
,sockfd(-1)
{}
void init()
{
//1.创建套接字
sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd < 0)
{
lg(Fatal,"sockfd create error %d", sockfd);
exit(SockfdErr);
}
lg(Info,"sockfd create success %d", sockfd);
//2.bind:将用户设置的的ip和port在内核中和我们当前的进程相关联
struct sockaddr_in local;
bzero(&local,0);
local.sin_family = AF_INET;
local.sin_port = htons(_port); //主机转网络 h表示host,n表示network
local.sin_addr.s_addr = inet_addr(_ip.c_str()); //1. string -> uint32_t 2. uint32_t必须是网络序列的 //
if(bind(sockfd,(const struct sockaddr*)&local,sizeof(local)) < 0)
{
lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
exit(BindErr);
}
lg(Info,"bind success!");
}
void start()
{
char buffer[1024];
while(true)
{
//纯输出型参数
struct sockaddr_in client;
//清空数据
bzero(&client,0);
socklen_t len = sizeof(client);
ssize_t n = recvfrom( sockfd , buffer,sizeof(buffer)-1, 0 , (struct sockaddr*)&client , &len);
if(n < 0)
{
lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
continue;
}
buffer[n] = 0;
uint16_t port = ntohs(client.sin_port); //client是从网络来,我们要转为主机字序
std::string ip = inet_ntoa(client.sin_addr); //4字节的网络序列要转化回本主机的字符串风格的IP
sendto(sockfd, buffer, strlen(buffer), 0, (const sockaddr*)&client, len);
}
}
~UdpServer()
{
if(sockfd>0) close(sockfd);
}
private:
int sockfd; //网络套接字描述符
std::string _ip; //IP地址
uint16_t _port; //端口号
};
cpp
#include "UdpServer.hpp"
#include <memory>
void Usage(std::string s)
{
std::cout << "\n\rUsage: " << s << " 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]);
UdpServer* ptr = new UdpServer(port); //创建一个服务器对象
ptr->init(); //初始化服务器对象
ptr->start(); //启动服务器
return 0;
}
log.hpp是我们自己写的日志文件系统,在之前的博客也有讲解。
运行结果如下:

这里阻塞了,因为我们还没写客户端
这里有个补充,我们多次使用inet_ntoa这个函数时,我们先来看一段代码


因为 inet_ntoa 把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖掉上一次的结果。
如果有多个线程调用 inet_ntoa,是否会出现异常情况呢?
- 在 APUE 中,明确提出 inet_ntoa 不是线程安全的函数。
- 但是在 centos7 上测试并没有出现问题,可能内部的实现加了互斥锁。
- 在多线程环境下,推荐使用 inet_ntop,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题。
(3)客户端的实现
客户端要能与服务器进行连接,就要知道服务器的IP和端口号,但这个实际是要我们自己填写的。

client 要不要 bind?
要,但是一般 client 不会显示的 bind,程序员不会自己 bind。
client 是一个客户端 -> 普通人下载安装启动使用的 -> 如果程序员自己 bind 了-> client 一定 bind 了一个固定的 ip 和 port,那万一其他的客户端提前占用了这个 port 呢?
client 一般不需要显示的 bind 指定 port,而是让 OS 自动随机选择。
这里我们发送数据用sendto函数
这里的参数和前面讲的 recvfrom 差不多,而这里的结构体内部需要自己填充目的 IP 和目的端口号。

cpp
#include <iostream>
#include <string.h>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
void Usage(const std::string& s)
{
std::cout << "\n\rUsage: " << s << " serverip serverport\n" << std::endl;
}
int main(int argc , char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(0);
}
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd < 0)
{
std::cerr << "socket error!" <<std::endl;
exit(1);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
struct sockaddr_in Server;
bzero(&Server,0);
Server.sin_family = AF_INET;
Server.sin_port = htons(serverport);
Server.sin_addr.s_addr = inet_addr(serverip.c_str());
char buffer[1024];
std::string message;
socklen_t len = sizeof(Server);
while(true)
{
std::cout << "Client say# ";
std::getline(std::cin,message);
//当Client首次给服务器发送消息的时候,OS会自动bind Client的port和IP
sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&Server,len);
struct sockaddr_in temp;
socklen_t temp_len = sizeof(temp);
ssize_t n = recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(sockaddr*)&temp,&temp_len);
if(n > 0)
{
buffer[n] = 0;
std::cout << "Server say$ " << buffer << std::endl;
}
}
close(sockfd);
return 0;
}
(4)让服务器与客户端实现通信
我们看到客户端的端口号是随机值。

这里的 127.0.0.1 叫做本地环回。client 和 server 发送数据只在本地协议栈中进行数据流动,不会将我们的数据发送到网络中。
作用:用来做本地网络服务器代码测试的,意思就是如果我们绑定的 IP 是 127.0.0.1 的话,在应用层发送的消息不会进入物理层,也就不会发送出去。
当我们运行起来后想要查看网络情况就可以用指令 netstat,后边也可以附带参数:
- -a:显示所有连线中的 Socket。
- -e:显示网络其他相关信息。
- -i:显示网络界面信息表单。
- -l:显示监控中的服务器的 Socket。
- -n:直接使用 ip 地址(数字),而不通过域名服务器。
- -p:显示正在使用 Socket 的程序识别码和程序名称。
- -t:显示 TCP 传输协议的连线状况。
- -u:显示 UDP 传输协议的连线状况。
客户端多线程处理收到消息和发送消息
cpp
#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>
#include "Terminal.hpp"
using namespace std;
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
<< std::endl;
}
struct ThreadData
{
struct sockaddr_in server;
int sockfd;
std::string serverip;
};
void *recv_message(void *args)
{
// OpenTerminal();
ThreadData *td = static_cast<ThreadData *>(args);
char buffer[1024];
while (true)
{
memset(buffer, 0, sizeof(buffer));
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len);
if (s > 0)
{
buffer[s] = 0;
cerr << buffer << endl;
}
}
}
void *send_message(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
string message;
socklen_t len = sizeof(td->server);
std::string welcome = td->serverip;
welcome += " comming...";
sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), len);
while (true)
{
cout << "Please Enter@ ";
getline(cin, message);
// std::cout << message << std::endl;
// 1. 数据 2. 给谁发
sendto(td->sockfd, message.c_str(), message.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]);
struct ThreadData td;
bzero(&td.server, sizeof(td.server));
td.server.sin_family = AF_INET;
td.server.sin_port = htons(serverport); //?
td.server.sin_addr.s_addr = inet_addr(serverip.c_str());
td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (td.sockfd < 0)
{
cout << "socker error" << endl;
return 1;
}
td.serverip = serverip;
pthread_t recvr, sender;
pthread_create(&recvr, nullptr, recv_message, &td);
pthread_create(&sender, nullptr, send_message, &td);
pthread_join(recvr, nullptr);
pthread_join(sender, nullptr);
close(td.sockfd);
return 0;
}
我们看到客户端有一个主线程和两个收到发送的线程

UDP(Windows 环境下 C++ 实现)
在 Windows 下写客户端,在 Linux 下用 Linux 充当服务器实现客户端发送数据,服务器接收数据的功能(Windows 下的套接字和 Linux 下的几乎一样)。
Windows环境下的Client.cpp
cpp
#define _WINSOCK_DEPRECATED_NO_WARNINGS 1
#include <iostream>
#include <WinSock2.h>
#include <Windows.h>
#include <string>
using namespace std;
uint16_t port = 8080;
std::string serverip = "8.155.26.31";
#pragma comment(lib, "ws2_32.lib")
int main()//_tmain,要加#include <tchar.h>才能用
{
WSADATA WSAData; //初始化信息
WORD sockVersion = MAKEWORD(2, 2);
//启动Winsock
if (WSAStartup(sockVersion,&WSAData) != 0) {
cout << "WSAStartup Error = " << WSAGetLastError() << endl;
return 0;
}
else {
cout << "start Success" << endl;
}
//创建socket
SOCKET clientSocket = socket(AF_INET, SOCK_DGRAM, 0);
if (clientSocket == SOCKET_ERROR) {
cout << "socket Error = " << WSAGetLastError() << endl;
return 1;
}
else {
cout << "socket Success" << endl;
}
sockaddr_in dstAddr;
dstAddr.sin_family = AF_INET;
dstAddr.sin_port = htons(port);
dstAddr.sin_addr.S_un.S_addr = inet_addr(serverip.c_str());
char buffer[1024];
while (true)
{
std::string message;
std::cout << "Client say# ";
std::getline(std::cin, message);
sendto(clientSocket, message.c_str(), (int)message.size(), 0, (struct sockaddr*)&dstAddr, sizeof(dstAddr));
struct sockaddr_in temp;
int len = sizeof(temp);
int s = recvfrom(clientSocket, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
if (s > 0)
{
buffer[s] = 0;
std::cout << "server say$ " << buffer << std::endl;
}
}
//关闭socket连接
closesocket(clientSocket);
WSACleanup();
return 0;
}
运行结果如下:

这里要实现正常通信,云服务器要进行被远程访问,就需要开放公网 IP 的端口
如何开放自己的端口我稍后会给大家写一篇博客讲解。