目录
预备知识
socket通信的本质
认识TCP协议和UDP协议
网络字节序
socket编程流程
socket编程时常见的函数
服务端绑定
整数IP和字符串IP
客户端套接字的创建和绑定
预备知识
理解源IP和目的IP
源IP指的是发送数据包的主机的IP地址,目的IP指的是接收数据包的主机的IP地址。在网络通信中,当一台主机需要向另一台主机发送数据包时,它需要将数据包的目的IP地址设置为接收主机的IP地址,同时在数据包的头部中加入源IP地址,以便接收主机能够知道数据包来自哪个主机。
理解源MAC地址和目的MAC地址
源MAC地址和目的MAC地址是数据链路层中的两个重要概念,MAC地址是每个网卡独有的物理地址,用于在局域网中标识设备。
源MAC地址是指发送数据包的主机的网卡的MAC地址,目的MAC地址是指接收数据包的主机的网卡的MAC地址。在数据包传输过程中,数据包的源MAC地址和目的MAC地址会随着数据包一起传输,并被中间的路由器或交换机使用来判断数据包的转发。
当数据包从源主机发送时,它首先需要找到目的主机所在的网卡,这时候就需要目的MAC地址。发送主机会将目的MAC地址填入数据包中,以便路由器或交换机能够将数据包准确地发送给目的主机。而源MAC地址则用于标识数据包的来源,这样目的主机就能够知道数据包是从哪个主机发送过来的
理解源端口号和目的端口号
在TCP/IP协议中,源端口号和目的端口号是定义在传输层的概念,用于在不同主机之间的通信中唯一标识一个虚拟连接。源端口号和目的端口号组成了一个socket,可以理解为一个网络应用程序的地址。
源端口号是发送方的端口号,目的端口号是接收方的端口号。在发送数据时,网络应用程序会将数据发送给目标主机的特定端口号,而接收方则会通过监听特定端口号来接收发送方传来的数据。
端口号的范围是0~65535,其中0~1023是系统端口号,已经被分配给一些知名的服务或应用程序,比如80号端口是HTTP服务默认端口、 21号端口是FTP服务默认端口、25号端口是SMTP服务默认端口。而1024~65535是动态端口号,可以由应用程序自行分配使用。
通过源端口号和目的端口号,TCP/IP协议可以建立并维护一条虚拟连接,保证数据包的可靠传输,并可以区分不同的应用程序之间的通信。
socket通信的本质
Socket通信的本质是建立在TCP/IP协议之上的一种应用层协议,它用于在网络上实现进程之间的通信。在Socket通信中,进程可以作为客户端或服务器端,通过IP地址和端口号建立连接并进行数据传输。
在建立Socket连接时,客户端会向特定IP地址和端口号发送连接请求,服务器端会接受连接请求并建立连接。一旦连接建立成功,客户端和服务器端就可以通过Socket实例进行数据的收发。
Socket通信的本质是通过TCP/IP协议在网络上传输数据,确保数据的可靠性和稳定性。TCP协议提供可靠的数据传输和流量控制,IP协议提供数据包的路由和分发。通过Socket通信,可以建立一对一、一对多、多对多的通信方式,实现进程之间的高效通信,广泛应用于互联网、局域网和各种分布式系统中。
总结:通过IP地址和MAC地址可以实现数据有一台主机传输到另一台主机了,但在实际生活中我们希望的是能将数据传输到指定的应用中(在机器上称进程),例如我们在自己主机上的京东APP上点赞一个店家时我们希望这个数据会传输到其它主机上的京东APP上,而不是淘宝APP或其它APP。这时就需要使用端口号来标识特定的进程APP。
在不同的两台主机上,可能会同时存在多个正在进行跨网络通信的进程,当数据到达目的主机时就需要通过端口号找到该主机上对应的通信进程,然后将数据交给该进程进行处理。而该主机也需要记住发送端的信息,当数据处理完后返还处理结果。
端口号的理解
端口号(port)是传输层协议的内容
端口号是一个2字节16位的整数;
端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
IP地址 + 端口号能够标识网络上的某一台主机的某一个进程; 一个端口号只能被一个进程占用。
理解 "端口号" 和 "进程ID"
到这里有人会问当数据到达目的主机时,为何不以进程PID为根据将数据发送给目的进程?端口号(prot)的作用和进程ID具有同样的功能,都是唯一标识一台主机上的某一个进程。为何还要创建出端口号?
首先进程ID是用于唯一标识系统内所有进程的,属于系统级概念,端口号用于唯一标识系统内要进行跨网络通信的进程,属于网络概念。并不是所有的进程都需要进行跨网络通信。
当数据到达目的主机时如何找到要接收数据的端口号?
底层采用哈希表方式建立端口号和进程IPD或PCB之间的映射,当底层拿到端口号时通过算法找到对应的进程。
认识TCP协议和UDP协议
TCP协议
TCP协议(Transmission Control Protocol,传输控制协议)是一种可靠的、面向连接的协议。它主要用于在计算机网络上提供可靠的数据传输服务。
TCP协议是一种面向字节流的协议,它通过将数据分成一系列的数据包进行传输,从而保证了数据的可靠性。在TCP连接建立后,通信双方将通过三次握手建立连接。数据传输过程中,TCP协议会对每一个数据包进行确认和校验,确保数据的完整性和正确性。当接收方接收到一个数据包时,会向发送方发送一个确认消息,确保发送方已经正确地发送了该数据包。如果发送方在一定时间内没有收到确认消息,它会重新发送相同的数据包,以确保数据的可靠传输。
总结:
传输层协议
有连接
可靠传输
面向字节流
UDP协议
UDP协议(User Datagram Protocol,用户数据报协议)是一种简单的、无连接的网络传输协议。UDP协议的特点是速度快,但可靠性比较低。
UDP协议是一种面向数据报的协议,它将数据分割成一个个独立的数据包进行传输,这些数据包被称为用户数据报(UDP Datagram)。在传输数据的过程中,UDP协议不会进行数据的确认和校验,也不会保证数据的顺序性。因此,UDP协议比TCP协议要快很多,但会存在数据丢失的风险。
总结:
传输层协议
无连接
不可靠传输
面向数据报
网络字节序
网络字节序,也叫大端字节序,是一种字节序(byte order)规范,它规定了数据在网络传输时的排序方式。
计算机数据存储模式
大端模式:数据的高字节存储在低地址,低字节存储在高地址。
小端模式:数据的高字节存储在高地址,低字节存储在低地址。
在网络传输中,不同计算机上的处理器可能采用不同的字节序,为了保证数据在网络中传输时的正确性,就需要使用统一的字节序,即网络字节序。在网络字节序中,整数类型的数据的高字节存储在低地址,低字节存储在高地址,与大端字节序相同。而在小端字节序中,整数类型的数据的高字节存储在高地址,低字节存储在低地址。因此,在网络传输中,数据需要转换为网络字节序,才能确保不同计算机之间的数据传输的正确性。
网络字节序是一种字节序规范,它规定了数据在网络传输时的排序方式,能够保证不同计算机之间的数据传输的正确性。
socket编程流程
sockaddr结构
套接字属于进程间通信中的一种,它支持本地通信和网络通信。在使用套接字进行跨网络通信是我们需要指明IP地址和端口号,而使用于本地通信则不需要,因此套接字提供了sockaddr_in结构体和sockaddr_un结构体,其中sockaddr_in结构体用于跨网络通信,而sockaddr_un结构体用于本地通信,网络让套接字的本地通信和跨网络通信都能使用一套相同的接口,系统还提供了sockaddr结构体。这三个结构体的头部的16个比特位都是一样的,这个字段叫协议家族。
所以我们在传参是就不用传入sockaddr_in或sockaddr_un这样的结构体,而使用统一的sockaddr,我们通过设置协议家族参数来表明我们要进行本地通信还是网络通信,这样套接字的本地通信和网络通信就得到了统一。
注意:在编写程序时,还是使用相应得结构体,如要进行网络通信时,定义时仍使用sockaddr_in,但在传参时强转为sockaddr*。
socket编程时常见的函数
socket( )
socket( )是一个系统调用,用于在应用程序中创建一个新的套接字(socket)。
该函数的原型如下:
int socket(int domain, int type, int protocol);
函数参数说明:
domain
:套接字通信的协议族,相当于struct sockaddr结构体的前16位,如果使用于本地通信就设置为AF_UNIX,如果使用于网络通信就设置AF_INET(IPv4协议)和AF_INET6
(IPv6协议)等。type
:套接字的类型,常见的有SOCK_STREAM
(面向连接的TCP协议)和SOCK_DGRAM
(无连接的UDP协议)等。protocol
:指定协议,通常为0,表示使用默认协议。
函数返回值:
- 成功:返回一个socket的文件描述符,可以用于后续网络通信中。
- 失败:返回-1,并设置errno全局变量表示具体错误原因。
当我们在程序中执行了socket函数时,底层会进行以下几个步骤:
内核创建一个新的socket对象,并为它分配唯一的文件描述符。
内核为此socket分配相关的资源,包括缓冲区、状态信息等。
内核为此socket分配本地端口号(如果是TCP协议的话),并对本地端口进行绑定(bind)操作,使得其它进程可以通过这个端口号来与此socket进行通信。
内核返回新socket对象的文件描述符,程序可以通过这个文件描述符来访问此socket。
当进程调用socket函数时,实际就相当于打开了一个“网络文件”,这时进程就要对这个"网络文件"进行管理,就要为这个"网络文件"分配文件描述符,这个文件描述符会作为socket函数的返回值返回用户,与普通的磁盘文件不同的是,磁盘文件是将数据刷新到磁盘上就完成了数据的操作,而网络文件则是将文件缓冲区的数据将会刷新到网卡里而卡是负责数据发送的,最终会将数据发送到网络中。
创建套接字
进行创建套接字时,要根据使用场景而填入相应的参数,这里我们使用UDP进行网络通信,所以协议族填AF_INET,SOCK_DGRAM,第三个参数填0为默认。
class udpserver
{
public:
bool InitServer()
{
//创建套接字
_sockfd=socket(AF_INET,SOCK_DGRAM,0);
//AF_INET//表示要进行网络通信,SOCK_DGRAM UDP协议
if(_sockfd<0)
{//表示创建套接字失败
std::cout<<" socket error";
return false;
}
std::cout<<" socket create success,socfd:"<<_sockfd<< std::endl;
return true;
}
~udpserver()
{
if(_sockfd>0)
{
close(_sockfd);//关闭文件描述符
}
}
private:
int _sockfd;//网络文件描述符
};
#include"server.hpp"
int main()
{
udpserver* ser=new udpserver();
ser->InitServer();
return 0;
}
运行结果:
服务端绑定
套接字创建好后,就相当于打开了一个文件,这个文件和其他文件并没有什么不同;操作系统并不知道要将文件的内容写到磁盘还网卡,所以我们需要将这个文件与一个本地地址进行绑( 通常指IP地址和端口号) , 这就不得不介绍bind函数。
bind函数是一个系统调用,它在应用程序的套接字(socket)和本地地址(IP地址和端口号)之间建立一个关联关系,使得其他应用程序可以通过该本地地址与该套接字进行通信。bind函数的主要作用是将一个特定的套接字绑定到一个特定的本地地址上,使得该套接字在网络上唯一地标识一个通信节点,从而实现网络通信。bind函数的函数原型如下:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
其中,sockfd是由socket函数返回的套接字描述符,addr是一个指向本地地址结构体的指针,addrlen是该结构体的长度。bind函数的调用方法如下:
struct sockaddr_in addr;
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建一个TCP套接字
memset(&addr, 0, sizeof(addr)); // 清空地址结构体
addr.sin_family = AF_INET; // 设置地址族为IPv4
addr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置本地地址为任意IP地址
addr.sin_port = htons(8080); // 设置本地端口号为8080
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { // 绑定套接字和本地地址
perror("bind error");
exit(EXIT_FAILURE);
}
在上面的例子中,我们首先通过socket函数创建了一个TCP套接字,然后创建了一个本地地址结构体addr,该结构体包含了本地IP地址和端口号等信息,然后将该结构体和套接字描述符作为参数传入bind函数中,调用bind函数将套接字和本地地址绑定起来。在绑定成功后,应用程序就可以通过该本地地址来访问该套接字,实现网络通信的功能。
#pragma once
#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include<unistd.h>
#include <unordered_map>
using namespace std;
class udpserver
{
public:
udpserver(std::string ip,int port):_sockfd(-1),_port(port),_ip(ip)
{}
bool InitServer()
{
//创建套接字
_sockfd=socket(AF_INET,SOCK_DGRAM,0);
//AF_INET//表示要进行网络通信,SOCK_DGRAM UDP协议
if(_sockfd<0)
{//表示创建套接字失败
std::cout<<" socket error";
return false;
}
std::cout<<" socket create success,socfd:"<<_sockfd<< std::endl;
struct sockaddr_in local;
memset(&local,'\0',sizeof(local));//清空结构体
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=inet_addr(_ip.c_str());
//绑定
if(bind(_sockfd,(struct sockaddr*)&local,sizeof(sockaddr))<0)
{
std::cout<<"bind error"<<std::endl;
return false;
}
std::cout<<"bind success"<<std::endl;
return true;
}
~udpserver()
{
if(_sockfd>0)
{
close(_sockfd);//关闭文件描述符
}
}
private:
int _sockfd;//网络文件描述符
int _port;//端口号
std::string _ip;//IP地址
};
#include"server.hpp"
int main(int argc,char*argv[])
{
if(argc!=2)
{
std:cout<<"usage:"<<argv[0]<<" port"<<std::endl;
return 1;
}
std::string ip="127.0.0.1";
int port=atoi(argv[1]);
udpserver* ser=new udpserver(ip,port);
ser->InitServer();
return 0;
}
整数IP和字符串IP
IP地址是Internet Protocol的缩写,是一个32位二进制数,通常表示为四个8位二进制数,即四个数字,每个数字范围在0到255之间,用点分十进制形式表示。例如:192.168.1.1
IP地址的表示形式有两种:
整数IP:将点分十进制形式表示的IP地址转换为一个32位整数,例如:192.168.1.1可以转换为十进制数3232235777。
字符串IP:即点分十进制形式表示的IP地址,例如:192.168.1.1。
整数IP转为字符串IP
将整数IP转换为字符串IP的主要原因是方便人们阅读和理解IP地址。字符串IP的形式更符合人们的阅读习惯,同时也更容易记忆。在实际应用中,比如在配置网络设备和编写网络应用程序时,我们通常使用字符串IP来表示IP地址,因为这更符合人们的直觉和需要。
inet_ntoa()函数是一个用于将32位无符号整数表示的IP地址转换为点分十进制字符串表示的网络函数。它的头文件为"arpa/inet.h",使用时需要包含该头文件。
该函数的原型如下:
char *inet_ntoa(struct in_addr in);
参数in是一个struct in_addr类型的结构体,表示待转换的32位无符号整数表示的IP地址。函数返回值是转换后的点分十进制字符串表示的IP地址,如果转换失败则返回NULL。
例如,以下代码将32位无符号整数表示的IP地址192.168.0.1转换为点分十进制字符串,并打印出转换后的结果:
inet_ntoa()函数是一个用于将32位无符号整数表示的IP地址转换为点分十进制字符串表示的网络函数。它的头文件为"arpa/inet.h",使用时需要包含该头文件。
该函数的原型如下:
char *inet_ntoa(struct in_addr in);
参数in是一个struct in_addr类型的结构体,表示待转换的32位无符号整数表示的IP地址。
函数返回值是转换后的点分十进制字符串表示的IP地址,如果转换失败则返回NULL。
例如,以下代码将32位无符号整数表示的IP地址192.168.0.1转换为点分十进制字符串,并打印出转换后的结果:
#include <stdio.h>
#include <arpa/inet.h>
int main() {
in_addr_t ip_int = 3232235521; // 192.168.0.1的32位无符号整数表示
struct in_addr in;
in.s_addr = ip_int;
char* ip_str = inet_ntoa(in);
if (ip_str == NULL) {
printf("IP address conversion error!\n");
return -1;
} else {
printf("IP address in string format: %s\n", ip_str);
return 0;
}
}
需要注意的是,由于inet_ntoa()函数返回的是指向静态缓冲区的指针,因此每次调用该函数时返回的字符串都会被覆盖。如果需要保存转换后的IP地址字符串,可以使用strdup()函数对其进行复制。
recvfrom()函数是一个用于在UDP协议中接收数据的网络函数。它的头文件为<sys/socket.h>,使用时需要包含该头文件。
该函数的原型如下:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
参数说明:
参数sockfd为已经创建好的socket文件描述符,用于接收数据;
参数buf为接收数据的缓冲区;参数len为缓冲区的长度,即最多接收的字节数;
参数flags一般为0,表示没有特殊要求;
参数src_addr为指向存放发送者IP地址和端口号的sockaddr结构体指针;
参数addrlen为指向sockaddr结构体长度的指针,接收到的发送者地址长度会写入其中。
返回值:函数返回值为接收到的字节数,如果出错则返回-1。
注意:udp是不面向连接的,所以在使用udp协议进行通信时,我们在获取数据信息时,还要获取数据发送方的有关属性,包括IP地址和端口号,要将struct sockadd_in* 转为更适合传输的struct sockadd*类型。
例如,以下代码从创建好的socket文件描述符sock_fd中接收数据,并输出接收到的信息:
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
int main() {
int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_fd == -1) {
printf("Failed to create socket!\n");
return -1;
}
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(8888);
if (bind(sock_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
printf("bind error!\n");
return -1;
}
char buf[1024] = {0};
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
ssize_t recv_len = recvfrom(sock_fd, buf, sizeof(buf), 0, (struct sockaddr *)&cli_addr, &cli_len);
if (recv_len == -1) {
printf("recvfrom error!\n");
return -1;
}
printf("Received message from %s:%d, message: %s\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), buf);
close(sock_fd);
return 0;
}
此处为UDP服务器代码,创建套接字,进行bind()操作后等待客户端发送数据,使用recvfrom()函数接收数据,然后输出接收到的信息。
#pragma once
#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include<unistd.h>
#include <unordered_map>
using namespace std;
class udpserver
{
public:
udpserver(std::string ip,int port):_sockfd(-1),_port(port),_ip(ip)
{}
bool InitServer()
{
//创建套接字
_sockfd=socket(AF_INET,SOCK_DGRAM,0);
//AF_INET//表示要进行网络通信,SOCK_DGRAM UDP协议
if(_sockfd<0)
{//表示创建套接字失败
std::cout<<" socket error";
return false;
}
std::cout<<" socket create success,socfd:"<<_sockfd<< std::endl;
struct sockaddr_in local;
memset(&local,'\0',sizeof(local));//清空结构体
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=inet_addr(_ip.c_str());
//绑定
if(bind(_sockfd,(struct sockaddr*)&local,sizeof(sockaddr))<0)
{
std::cout<<"bond error"<<std::endl;
return false;
}
std::cout<<"bond success"<<std::endl;
return true;
}
void Start()
{
#define SIZE 1024
char buffer[SIZE];
while(true)
{
struct sockaddr_in perr;
socklen_t len=sizeof(perr);
//读取数据
ssize_t size=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&perr,&len);
if(size<0)//读取失败
{
std::cout<<"recvfrom cerror"<<std::endl;
}
else
{
buffer[size]='\0';
int port=ntohs(perr.sin_port);
std::string ip=inet_ntoa(perr.sin_addr);
std::cout<<ip<<": "<<port<<"# "<<buffer<<std::endl;
}
}
}
~udpserver()
{
if(_sockfd>0)
{
close(_sockfd);//关闭文件描述符
}
}
private:
int _sockfd;//网络文件描述符
int _port;//端口号
std::string _ip;//IP地址
};
#include"server.hpp" int main(int argc,char*argv[]) { if(argc!=2) { std:cout<<"usage:"<<argv[0]<<" port"<<std::endl; return 1; } std::string ip="127.0.0.1"; int port=atoi(argv[0]); udpserver* ser=new udpserver(ip,port); ser->InitServer(); ser->Start(); return 0; }
客户端套接字的创建和绑定
客户端在创建套接字时参数的传递与服务端创建套接字时一样都是AF_INET, SOCK_DGRAM, 0,不同的是服务端需要进行端口号绑定,而客户端不需要。
但在网络通信,通信双方都需要找到对方,因此服务端和客户端都需要有各自的IP地址和端口号,只不过服务端需要进行端口号的绑定,而客户端不需要。
因为服务器就是为了给别人提供服务的,因此服务器必须要让别人知道自己的IP地址和端口号,IP地址一般对应的就是域名,而端口号一般没有显示指明过,因此服务端的端口号一定要是一个众所周知的端口号,并且选定后不能轻易改变,否则客户端是无法知道服务端的端口号的,这就是服务端要进行绑定的原因,只有绑定之后这个端口号才真正属于自己,因为一个端口只能被一个进程所绑定,服务器绑定一个端口就是为了独占这个端口。
而客户端在通信时虽然也需要端口号,但客户端一般是不进行绑定的,客户端访问服务端的时候,端口号只要是唯一的就行了,不需要和特定客户端进程强相关。
客户端不需要绑定端口号是因为客户端的套接字在创建时会自动由操作系统分配一个随机端口号,并且这个随机端口号会在套接字连接到服务器端时被发送给服务器端,因此服务器端就可以使用该端口号来与客户端进行数据传输。客户端套接字的随机端口号一般是在1024到65535之间的一个空闲端口号,这样可以避免与其他已知端口号冲突。
如果客户端绑定了某个端口号,那么以后这个端口号就只能给这一个客户端使用,就是这个客户端没有启动,这个端口号也无法分配给别人,并且如果这个端口号被别人使用了,那么这个客户端就无法启动了。所以客户端的端口只要保证唯一性就行了,因此客户端端口可以动态的进行设置,并且客户端的端口号不需要我们来设置,当我们调用类似于sendto这样的接口时,操作系统会自动给当前客户端获取一个唯一的端口号。
也就是说,客户端每次启动时使用的端口号可能是变化的,此时只要我们的端口号没有被耗尽,客户端就永远可以启动。
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include<unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class udpclient
{
public:
udpclient(std::string server_ip,int server_port)
:_sockfd(-1)
,_server_port(server_port)
,_server_ip(server_ip)
{}
bool Initclient()
{
//创建套接字
_sockfd=socket(AF_INET,SOCK_DGRAM,0);
//AF_INET//表示要进行网络通信,SOCK_DGRAM UDP协议
if(_sockfd<0)
{//表示创建套接字失败
std::cout<<" socket error";
return false;
}
return true;
}
udpclient()
{
if(_sockfd>0)
{
close(_sockfd);//关闭文件描述符
}
}
private:
int _sockfd;//文件描述符
int _server_port;//服务端端口号
std::string _server_ip;//服务端的IP地址
};
当客户端与服务端进行绑定后接下来就是要向对方发送消息进行通信了。
UDP通信中的sendto函数用于向指定的IP地址和端口号发送数据报。该函数的调用格式如下:
int 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地址和端口号分别填入该结构体的sin_addr和sin_port成员中。
- addrlen:目的地址结构体的长度。
调用sendto函数时需要填写目的地址结构体,例如:
struct sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(port);
dest_addr.sin_addr.s_addr = inet_addr(ip);
其中,port和ip分别为目的端口号和IP地址字符串。
sendto函数会将指定的数据报发送到指定的目的地址,如果发送成功返回发送的字节数,如果失败返回-1,并设置全局errno变量的值。由于UDP是无连接的,因此在发送数据时不需要建立连接。
UDP通信中的sendto函数用于向指定的IP地址和端口号发送数据报。该函数的调用格式如下:
int 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地址和端口号分别填入该结构体的sin_addr和sin_port成员中。
- addrlen:目的地址结构体的长度。
调用sendto函数时需要填写目的地址结构体,例如:
struct sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(port);
dest_addr.sin_addr.s_addr = inet_addr(ip);
其中,port和ip分别为目的端口号和IP地址字符串。
sendto函数会将指定的数据报发送到指定的目的地址,如果发送成功返回发送的字节数,如果失败返回-1,并设置全局errno变量的值。由于UDP是无连接的,因此在发送数据时不需要建立连接。
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include<unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class udpclient
{
public:
udpclient(std::string server_ip,int server_port)
:_sockfd(-1)
,_server_port(server_port)
,_server_ip(server_ip)
{}
bool Initclient()
{
//创建套接字
_sockfd=socket(AF_INET,SOCK_DGRAM,0);
//AF_INET//表示要进行网络通信,SOCK_DGRAM UDP协议
if(_sockfd<0)
{//表示创建套接字失败
std::cout<<" socket error";
return false;
}
return true;
}
void Start()
{
std::string msg;
struct sockaddr_in perr;
memset(&perr,'\0',sizeof(perr));
perr.sin_family=AF_INET;
perr.sin_port=htons(_server_port);
perr.sin_addr.s_addr=inet_addr(_server_ip.c_str());
while(true)
{
std::cout<<"Please Enter#";
getline(std::cin,msg);
sendto(_sockfd,msg.c_str(),msg.size(),0,(struct sockaddr*)&perr,sizeof(perr));
//std::cout<<"sendto() success"<<std::endl;
}
}
udpclient()
{
if(_sockfd>0)
{
close(_sockfd);//关闭文件描述符
}
}
private:
int _sockfd;//文件描述符
int _server_port;//服务端端口号
std::string _server_ip;//服务端的IP地址
};
#include"client.hpp"
int main(int argc,char*argv[])
{
if(argc!=3)
{
std::cout<<"usage:"<<argv[0]<<" port"<<std::endl;
return 1;
}
std::string server_ip=argv[1];
int server_port=atoi(argv[2]);
udpclient* cli=new udpclient(server_ip,server_port);
cli->Initclient();
cli->Start();
return 0;
}