文章目录
预备知识
IP和端口号
总体概述一下:就是网络通信本质上还是进程间通信只不过该通信需要跨网络和跨主机,因此我们就要明确要将数据发给谁,收到数据的进程要知道是谁给他发的数据,确保能够及时做出回应。这样就引出了以下的概念:
- IP:用来标识全公网唯一一台主机,端口号(2字节的16位整数):用来标识一台主机中唯一一个进程。
- 在IP数据包头部中,有两个IP地址,分别叫做源IP地址和目的IP地址。
- 传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫作源端口号和目的端口号。(描述数据是谁发的,发给谁的)
端口号和进程ID
上面说到端口号用来标识唯一一个进程,而进程ID也是用来标识唯一一个进程,那这两者之间是怎样的关系呢??
- 解释一下:就是每个进程都会有一个pid,但是不是所有进程都会有端口号,意思就是某些进程不需要跨网络通信所以它不需要端口号。
- 另外,一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定。
- 端口号由IANT(Internet Assigned Numbers Authority)管理,IANT将端口号分配如下:
简单认识UDP协议
属于传输层协议,无连接,不可靠传输,面向数据报。
网络字节序
我们知道了IP和端口号就可以将数据正确传输给目标进程吗??
当然不是,我们知道内存中的多字节数据想对于内存地址有大小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大小端之分,同样网络数据流也有大小端之分。我们需要了解他是怎样的!!!
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。这样就保证了数据的一致性。
- TCP/IP协议规定,网络字节流采用大端字节序,即低地址存高字节。
- 所以不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据。
- 如果当前发送主机是小端,就需要先将数据转成大段,大端就忽略直接发送。
为使网络程序具有可移植性,使同样的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位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
socket编程接口(本文主要围绕UDP)
原理图
本文主要用到的API
socket函数
#include<sys/socket.h>
int socket(int domain, int type, int protocol);
功能:创建套接字
返回值:成功返回一个文件描述符,出错返回-1。
参数如下表:
bind函数
#include<sys/socket.h>
#include<sys/types.h>
int bind(int sockfd, const struct sockaddr *address,socklen_t address_len);
功能:绑定本机IP和端口号。
返回值:成功返回0,失败-1。
参数如下:
虽然socket api的接口是sockaddr,但是由于在struct_sockaddr结构体中初始化IP和端口号不方便,便衍生出了Internet协议地址结构struct sockaddr_in,其形式如下:
但是,IP地址通常是字符串形式或点分十进制,例如:192.168.1.100。由于struct in_addr的成员s_addr 是无符号长整型,因此要进行变量转换,相关函数如下:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
(1) inet_addr函数
in_addr_t inet_addr(const char *cp);
功能:将点分十进制的IP地址转换为无符号长整形。
参数:cp 是以NULL结尾的点分IPV4字符串。
返回值:成功返回无符号长整形的IP地址,出错返回-1。
例如:addr=inet_addr("192.168.1.100")。
(2)inet_ntoa函数
char* inet_ntoa(struct in_addr in);
功能:将长整型IP地址转换为点分十进制。
参数:in是IPv4地址结构。
返回值:成功返回一指向包含点分IP地址的字符指针,出错返回NULL。
recvfrom函数和sendto函数(用于UDP中接收和发送数据)
#include <sys/socket.h>
ssize_t recvfrom(int socket, void *restrict buffer, size_t length, int flags,struct sockaddr *restrict address, socklen_t *restrict address_len);
参数说明:
socket :socket描述符
buffer : UDP数据报缓存地址
length :期望接收多长
falg :一般为0
restrict address:从哪接收的UDP数据报(不关心为NULL)
restrict address_len:接收的长度(不关心为NULL)
返回值:成功返回实际接收的字符长度,失败-1,错误原因存于errno 中
//#################################################################
ssize_t sendto(int socket, const void *message, size_t length,int flags, const struct sockaddr *dest_addr,socklen_t dest_len);
参数说明:
socket :socket描述符
message: 要发送的消息
length:期望发送消息长度
falgs:一般为0
dest_addr:往哪发即目的地址
dest_len:目标的地址长度
返回值:成功返回实际发送出去的字符长度,失败-1,错误原因存于errno 中
一个简单的UDP通信
// u_server.hpp
#pragma once
#include<iostream>
#include<string>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
class u_server{
private:
std::string ip;
int port;
int sock;
public:
u_server(std::string _ip="127.0.0.1",int _port=8080)
:ip(_ip),port(_port)
{}
void init_server()
{
sock=socket(AF_INET,SOCK_DGRAM,0);
std::cout<<"sock:"<<sock<<std::endl;
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(port);
local.sin_addr.s_addr=inet_addr(ip.c_str());
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
std::cerr<<"bind error!\n"<<std::endl;
exit(1);
}
}
//echo server
void start()
{
char msg[64];
while(true)
{
msg[0]='\0';
struct sockaddr_in end_point;
socklen_t len=(sizeof(end_point));
ssize_t s=recvfrom(sock,msg,sizeof(msg)-1,0,(struct sockaddr*)&end_point,&len);
if(s>0)
{
msg[s]='\0';
std::cout<<"client# "<<msg<<std::endl;
std::string echo_string=msg;
echo_string+="[server echo!]";
sendto(sock,echo_string.c_str(),echo_string.size(),
0,(struct sockaddr*)&end_point,len);
}
}
}
~u_server()
{
close(sock);
}
};
// u_server.cc
#include"u_server.hpp"
int main()
{
u_server *up=new u_server();
up->init_server();
up->start();
delete up;
return 0;
}
//##############################################
//u_client.hpp
#pragma once
#include<iostream>
#include<string>
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
class u_client{
private:
std::string ip;
int port;
int sock;
public:
u_client(std::string _ip="127.0.0.1",int _port=8080)
:ip(_ip),port(_port)
{}
void init_client()
{
sock=socket(AF_INET,SOCK_DGRAM,0);
std::cout<<"sock:"<<sock<<std::endl;
}
//echo client
void start()
{
std::string msg;
struct sockaddr_in peer;
peer.sin_family=AF_INET;
peer.sin_port=htons(port);
peer.sin_addr.s_addr=inet_addr(ip.c_str());
while(true)
{
std::cout<<"Please Enter# ";
std::cin>>msg;
if(msg=="quit"){
break;
}
sendto(sock,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));
char echo[128];
ssize_t s=recvfrom(sock,echo,sizeof(echo)-1,0,nullptr,nullptr);
if(s>0)
{
echo[s]='\0';
std::cout<<"server# "<<echo<<std::endl;
}
}
}
~u_client()
{
close(sock);
}
};
// u_client.cc
#include"u_client.hpp"
int main()
{
u_client *uc=new u_client();
uc->init_client();
uc->start();
delete uc;
return 0;
}
Makefile文件:
理解套接字的本质
当应用程序(客户或服务器)需要使用网络进行通信时,必须首先发出 socket 系统调用, 请求操作系统为其创建一 个 “套接字” 。这个调用的实际效果是请求操作系统把网络通信所需要的一些系统资源(存储器空间、CPU时间、网络带宽等)分配给相应的进程。操作系统把这些资源的总和用一个叫作套接字描述符(socket descriptor)的号码(本质就是文件描述符)来表示,然后把这个套接字描述符返回给应用进程。此后,应用进程所进行的网络操作(建立连接、收发数据、调整网络通信参数等)都必须使用这个套接字描述符(即socket函数返回值)。所以,几乎所有的网络系统都把这个套接字描述符作为套接字的许多参数中的第一个参数。在处理系统调用的时候,通过套接字描述符,操作系统就可以识别出应该使用哪些资源来完成应用进程所请求的服务。通信完毕后,应用进程通过一个关闭套接字的close 系统调用通知操作系统回收该套接字描述符相关的所有资源。由此可见,套接字是应用进程为了获得网络通信服务而与操作系统进行交互使用的一种机制。