一、网络编程基本知识
1.1 源IP地址和目的IP地址
-
在ip数据包头部,有两个ip地址,分别叫做源ip地址和目的ip地址
-
源ip地址标记着数据是从哪里发出来的,目的ip地址表示发往那台主机
IP地址标定全网内唯一一台主机
1.2 端口号和PID
- 端口号(port)是传输层协议的内容.
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程,进一步可认为全网内唯一的一个进程
- 一个端口号只能被一个进程占用
端口号标定了特定一台主机上的唯一进程
2. 端口号和pid的关系
- 每个进程都会有PID,PID也是进程的唯一标识
- 只有网络进程才有端口号
- 一个进程可以有多个端口号
1.3 认识TCP协议
我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题.
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
我们只要知道TCP是有连接的面向字节流的可靠传输,TCP要保证数据的可靠性,即不能丢失,因此效率相对来说更低。
1.4 认识UDP协议
此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面再详细讨论.
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
同样我们只要知道UDP是无连接面向数据报的不可靠传输,UDP不管数据可靠性,只负责传输数据,因此更加简单高效
1.5 网络字节序
- 对于我们用的笔记本电脑,通常都是小端机,即低地址放低权值位
- 对于我们人来说更习惯大端机读取方式,即低地址放高权值位,而我们又习惯从低地址开始读数据,如下面的栗子,
机器有大小端之分,那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
1.6 网络字节序转换接口
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
#include<arpa/inet.h> //头文件
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint_32 netlong)
uint16_t ntohs(uint_16 netshort)
速记原则
- h代表host主机,n代表network网络
- l代表32位长整数,s代表16位短整数
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
二、socket网络编程接口
2.1 socket 常见API
<1> 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
<2> 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
2.1 sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同.
- Pv4和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结构
<3> in_adr结构
三、简单的UDP网络程序
进行通信就需要收发数据,因此还需要两个接口用来收发数据
- 收数据
- 发数据
3.1 服务器端代码
- udpServer.hpp
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
class udpServer
{
private:
std::string _ip; //ip地址
int _port; //端口号
int _sock; //套接字文件描述符
public:
//构造函数
udpServer(std::string ip = "127.0.0.1", int port = 8080)
:_ip(ip)
,_port(port)
{
}
//初始化服务器
void initServer()
{
//1.创建套接字
_sock = socket(AF_INET, SOCK_DGRAM, 0);
std::cout << "sock:"<< _sock << std::endl;
//2.填重结构体协议,ip,_port
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_addr.s_addr = inet_addr(_ip.c_str());
local.sin_port = htons(_port);
//3.绑定端口,失败直接结束进程
if(bind(_sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
std::cerr << "bind failed!" << std::endl;
exit(1);
}
}
//启动服务器
void start()
{
//1.创建字符串接收数据
std::cout << "---";
char msg[64] = "\0";
while(1)
{
//定义远端addr结构体用于接收ip地址等
struct sockaddr_in end_point;
socklen_t len = sizeof(end_point);
ssize_t s = recvfrom(_sock, msg, sizeof(msg), 0, (struct sockaddr*)&end_point, &len);
if(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);
}
}
}
//析构函数
~udpServer()
{
close(_sock);
}
};
- udpServer.cc
这里我们借助命令行参数,规定使用该程序时需要输入IP地址和端口号
#include "udpServer.hpp"
void Usage(std::string proc)
{
std::cout << "Usage:" << proc << " server_ip server_port"<< std::endl;
}
//./udpServer ip port
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
udpServer *up = new udpServer(argv[1],atoi(argv[2]));
up->initServer();
up->start();
delete up;
return 0;
}
3.2 客户端代码
- udpClient.hpp
作为客户端只需要创建套接字,不需要绑定端口等操作
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
class udpClient
{
private:
std::string _ip; //ip地址
int _port; //端口号
int _sock; //套接字文件描述符
public:
//构造函数
udpClient(std::string ip = "127.0.0.1", int port = 8080)
:_ip(ip)
,_port(port)
{
}
//初始化
void initClient()
{
//1.创建套接字
_sock = socket(AF_INET, SOCK_DGRAM, 0);
std::cout << "sock:"<< _sock << std::endl;
// //2.填重结构体协议,ip,_port
// struct sockaddr_in peer;
// peer.sin_family = AF_INET;
// peer.sin_addr.s_addr = inet_addr(_ip.c_str());
// peer.sin_port = htons(_port);
// //3.绑定端口,失败直接结束进程
// if(bind(_sock,(struct sockaddr*)&peer,sizeof(local)) < 0)
// std::cerr << "bind failed!" << std::endl;
// exit(1);
}
//启动
void start()
{
std::string msg;
struct sockaddr_in peer;
peer.sin_family = AF_INET;
peer.sin_addr.s_addr = inet_addr(_ip.c_str());
peer.sin_port = htons(_port);
while(1)
{
std::cout << "Please enter your msg:#";
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;
}
}
}
//析构函数
~udpClient()
{
close(_sock);
}
};
- udpClient.cc
#include "udpClient.hpp"
void Usage(std::string proc)
{
std::cout << proc << "Server_IP" << " server_port" << std::endl;
}
int main(int argc, char * argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
udpClient *uc = new udpClient(argv[1],atoi(argv[2]));
uc->initClient();
uc->start();
delete uc;
return 0;
}
3.3 Makefile
.PHONY:all
all:udpClient udpServer
udpClient:udpClient.cc
g++ -o $@ $^ -std=c++11
udpServer:udpServer.cc
g++ -o $@ $^
.PHNOY:clean
clean:
rm -f udpClient udpServer
3.4 结果
转载:https://blog.csdn.net/qq_40076022/article/details/116765893