目录
前言
一、预备知识
1.1 源IP地址和目的IP地址
1.2 区分端口号和进程ID
1.3 TCP协议和UDP协议
1.4 网络字节序
二、socket编程接口
2.1 socket套接字的概念
2.2 socket常见API
2.3 sockaddr结构
三、关于IP和Port的绑定问题
四、编写简单的UDP服务端和客户端
前言
在进入本章内容学习前,我们先要了解网络编程是什么:
实际上网路编程指的是网络上的主机通过不同的进程,以编程的方式实现网络通信。
一、预备知识
1.1 源IP地址和目的IP地址
在IP数据包头部中,有两个IP地址,分别叫做源IP地址和目的IP地址,源IP地址和目的IP地址在进行网络通讯时是不会改变的,你可以理解为外出旅行时的出发地和目的地在旅行过程中是不会改变的。
1.2 区分端口号和进程ID
进程PID已经能够标识一台主机上进程的唯一性了,为什么还要搞一个端口号?
- 不是所有的进程都要网络通信,但所有的进程都要有PID;
- 更重要的说应该是要实现系统和网络功能的解耦。
IP标定互联网中唯一一台主机,端口号port用来标识该主机上的唯一的一个进程,因此
IP地址+端口号(port)能够标识全网内唯一的一个进程。
另外,一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定。
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号。就是在描述 "数据是谁发的,要发给谁。
1.3 TCP协议和UDP协议
TCP协议(Transmission Control Protocol 传输控制协议):
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
UDP协议(User Datagram Protocol 用户数据报协议):
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
1.4 网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。
- 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数;
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送;
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
二、socket编程接口
2.1 socket套接字的概念
由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。
基于Socket套接字的网络程序开发就是网络编程。
套接字编程的种类:
- 域间套接字编程(一般用于一个主机内的多个进程通信)
- 原始套接字编程(通常是网络工具)
- 网络套接字编程(用户间的网络通信)
2.2 socket常见API
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);(头文件均为<sys/socket.h>)
接收:
recvfrom() 函数用于从已连接或未连接的套接字接收数据,并将数据存储到缓冲区 buf 中。它的参数说明如下:
- sockfd:套接字描述符。
- buf:指向接收数据的缓冲区。
- len:缓冲区的大小。
- flags:额外的选项标志,通常设为 0。
- src_addr:指向 struct sockaddr 结构体(见2.3)的指针,用于存储发送方的地址信息。
- addrlen:指向 socklen_t 类型的指针,用于存储 src_addr 的长度。
- 函数返回值为
ssize_t
类型,表示接收到的数据的字节数。如果返回 -1,则表示接收出错
发送:
sendto()
函数用于向指定的目标地址发送数据。它的参数说明如下:
- sockfd:套接字描述符。
- buf:指向要发送数据的缓冲区。
- len:发送数据的大小。
- flags:额外的选项标志,通常设为 0。
- dest_addr:指向 struct sockaddr 结构体(见2.3)的指针,用于目标地址信息。
- addrlen:指向 socklen_t 类型的指针,用于存储 src_addr 的长度。
- 函数返回值为
ssize_t
类型,表示实际发送的数据的字节数。如果返回 -1,则表示接收出错
2.3 sockaddr结构
下面三种 sockaddr结构 分别对应:通用的网络地址结构体、IPv4地址结构体和Unix域地址结构体
接下来我们深入看看sockaddr_in的结构:
框内第一个元素是端口号,第二个元素又是一个结构体类型:
可以看到它其实就是一个四字节的整数。
在使用时端口号是要在网络里来回发送的,因此必须保证端口号是网络字节序列;
同样的,IP如果要被网络使用,那么它也必须是网络字节序列的。
如果要把字符串IP转成四字节整型是不是很麻烦呢?
其实系统早已为我们封装好了接口:
三、关于IP和Port的绑定问题
首先关于IP的绑定,若为0.0.0.0:
对于服务器:
- 当服务器将IP地址绑定为
0.0.0.0
并且指定了特定的端口号时,服务器将会监听该端口号,并可以接受来自任何可用网络接口的连接请求。- 这意味着服务器将在所有网络接口上等待连接,包括本地回环接口(
127.0.0.1
)和物理网络接口。- 当有连接请求到达时,服务器可以根据需要选择特定的网络接口来处理连接。
对于客户端:
- 当客户端将IP地址绑定为
0.0.0.0
并指定了特定的端口号时,客户端将通过任何可用的网络接口发送数据。- 这意味着客户端可以使用任何可用的网络接口来与目标服务器进行通信。
关于Port的绑定:
[0,1023]是系统内定的端口号,一般都有固定的应用层协议使用,如http:80 https:443
mysql:3306
四、编写简单的UDP服务端和客户端
UdpServer.hpp:
#pragma once#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"// using func_t = std::function<std::string(const std::string&)>;
typedef std::function<std::string(const std::string&)> func_t;Log lg;enum{SOCKET_ERR=1,BIND_ERR
};uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;class UdpServer{
public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0), port_(port), ip_(ip),isrunning_(false){}void Init(){// 1. 创建udp socketsockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // PF_INETif(sockfd_ < 0){lg(Fatal, "socket create error, sockfd: %d", sockfd_);exit(SOCKET_ERR);}lg(Info, "socket create success, sockfd: %d", sockfd_);// 2. bind socketstruct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port_); //需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的local.sin_addr.s_addr = inet_addr(ip_.c_str()); //1. string -> uint32_t 2. uint32_t必须是网络序列的 // ??// local.sin_addr.s_addr = htonl(INADDR_ANY);if(bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0){lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));}void Run(func_t func) // 对代码进行分层{isrunning_ = true;char inbuffer[size];while(isrunning_){struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);if(n < 0){lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}inbuffer[n] = 0;std::string info = inbuffer;std::string echo_string = func(info);sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (const sockaddr*)&client, len);}}~UdpServer(){if(sockfd_>0) close(sockfd_);}
private:int sockfd_; // 网路文件描述符std::string ip_; // 任意地址bind 0uint16_t port_; // 表明服务器进程的端口号bool isrunning_;
};
UdpClient.cpp:
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>using namespace std;void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}// ./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 sockaddr_in server;bzero(&server, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport); //?server.sin_addr.s_addr = inet_addr(serverip.c_str());socklen_t len = sizeof(server);int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){cout << "socker error" << endl;return 1;}// client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!// 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!// 系统什么时候给我bind呢?首次发送数据的时候string message;char buffer[1024];while (true){cout << "Please Enter@ ";getline(cin, message);// std::cout << message << std::endl;// 1. 数据 2. 给谁发sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, len);struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);if(s > 0){buffer[s] = 0;cout << buffer << endl;}}close(sockfd);return 0;
}
main.cpp:
#include "UdpServer.hpp"
#include <memory>
#include <cstdio>void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}std::string Handler(const std::string &str)
{std::string res = "Server get a message: ";res += str;std::cout << res << std::endl;return res;
}std::string ExcuteCommand(const std::string &cmd)
{// SafeCheck(cmd);FILE *fp = popen(cmd.c_str(), "r");if(nullptr == fp){perror("popen");return "error";}std::string result;char buffer[4096];while(true){char *ok = fgets(buffer, sizeof(buffer), fp);if(ok == nullptr) break;result += buffer;}pclose(fp);return result;
}// ./udpserver port
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpServer> svr(new UdpServer(port));svr->Init(/**/);svr->Run(Handler);return 0;
}