目录
1. 源IP地址与目的IP地址的认识
2. 端口号的认识
3. 套接字socket
4. TCP协议和UDP协议
5. 网络字节序
6. socket编程
7. socket编程接口
8. 使用UDP协议跨网络通信程序
Linux网络编程✨
1. 源IP地址与目的IP地址的认识
在因特网上,一台主机和一个IP地址往往是一一对应的。
但还有例外:一个网卡可以使用多个IP地址,但总的来说唯一的一个IP地址便可以确定一个主机;
主机A 有自己的一个IP地址,主机C 有自己的一个IP地址;中间的路由器跨两个网段,有两个IP地址;
使用路由器连接的两个不同网段的主机A 和主机C通信:
主机A要向主机C发送数据时,数据经过封装后,首先由主机A的IP地址转发给路由器左边端口的IP,经过路由器转发至右边的端口IP,最后转发至主机C的IP;
在这期间经过多个IP地址的转化,但源IP地址和目的IP地址是不变的;
源IP地址:主机A的IP地址;
目的IP地址:主机C的IP地址;
2. 端口号的认识
3. 套接字socket
4. TCP协议和UDP协议
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
注意:
上述两种协议并无好坏之分,具体场景选择合适的协议;
5. 网络字节序
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
总的来说:不管主机是大端存储还是小端存储,在网络数据流中,总是大端的;
- 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
6. socket编程
sockaddr结构
- 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结构体指针做为参数;
7. socket编程接口
socket:创建套接字:
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
返回值:
- 套接字创建成功返回一个文件描述符 = ,创建失败返回-1,同时错误码会被设置。
参数:
- domain:创建套接字的域或者叫做协议家族,也就是创建套接字的类型。该参数就相当于struct sockaddr结构的前16个位。如果是本地通信就设置为AF_UNIX,如果是网络通信就设置为AF_INET(IPv4)或AF_INET6(IPv6)。
- type:创建套接字时所需的服务类型。其中最常见的服务类型是SOCK_STREAM和SOCK_DGRAM,如果是基于UDP的网络通信,我们采用的就是SOCK_DGRAM,叫做用户数据报服务,如果是基于TCP的网络通信,我们采用的就是SOCK_STREAM,叫做流式套接字,提供的是流式服务。
- protocol:创建套接字的协议类别。你可以指明为TCP或UDP,但该字段一般直接设置为0就可以了,设置为0表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议。
struct sockaddr_in结构体
在绑定时需要将网络相关的属性信息填充到一个结构体当中,然后将该结构体作为bind函数的第二个参数进行传入,这实际就是struct sockaddr_in结构体。
struct sockaddr_in当中的成员如下:
- sin_family:表示通信机制(本地/网络)。
- sin_port:表示端口号,是一个16位的整数。
- sin_addr.s_addr:表示IP地址,是一个32位的整数。
剩下的字段一般不做处理,当然你也可以进行初始化。
其中sin_addr的类型是struct in_addr,实际该结构体当中就只有一个成员,该成员就是一个32位的整数,IP地址实际就是存储在这个整数当中的。
bind:绑定端口号:
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
返回值:
- 绑定成功返回0,绑定失败返回-1,同时错误码会被设置。
参数:
- socket:绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符。
- addr:网络相关的属性信息,包括协议家族、IP地址、端口号等。
- addrlen:传入的addr结构体的长度。
读取数据:recvfrom函数
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
返回值:
- 读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置。
参数:
- sockfd:对应操作的文件描述符。表示从该文件描述符索引的文件当中读取数据。
- buf:读取数据的存放位置。
- len:期望读取数据的字节数。
- flags:读取的方式。一般设置为0,表示阻塞读取。
- src_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
- addrlen:调用时传入期望读取的src_addr结构体的长度,返回时代表实际读取到的src_addr结构体的长度,这是一个输入输出型参数。
注意:
- 由于recvfrom函数提供的参数也是struct sockaddr类型的,因此我们在传入结构体地址时需要将struct sockaddr_in类型进行强转。
发送数据:sendto函数
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
返回值:
- 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。
参数:
- sockfd:对应操作的文件描述符。表示将数据写入该文件描述符索引的文件当中。
- buf:待写入数据的存放位置。
- len:期望写入数据的字节数。
- flags:写入的方式。一般设置为0,表示阻塞写入。
- dest_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。 addrlen:传入dest_addr结构体的长度。
8. 使用UDP协议跨网络通信程序
服务器:
- 1. 创建套接字;
- 2. 定义结构体;
- 3. 绑定端口;
- 4. 通信;
- 1. 创建套接字;
- 2. 定义结构体;
- 3. 通信;
- makefile
.PHONY:all
all:udp_server udp_client
udp_server:udp_server.cc
g++ -o $@ $^ -std=c++11
udp_client:udp_client.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f udp_client udp_server
- udp_server.cc
#include <iostream>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
const uint16_t port = 8080;
// udp_server,细节最后在慢慢完善
int main()
{
//1. 创建套接字,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0){
std::cerr << "socket create error: " << errno << std::endl;
return 1;
}
//2. 给该服务器绑定端口和ip(特殊处理)
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port); //此处的端口号,是我们计算机上的变量,是主机序列
// a. 需要将人识别的点分十进制,字符串风格IP地址,转化成为4字节整数IP
// b. 也要考虑大小端
// in_addr_t inet_addr(const char *cp); 能完成上面ab两个工作.
// 坑:
// 云服务器,不允许用户直接bind公网IP,另外, 实际正常编写的时候,我们也不会指明IP
// local.sin_addr.s_addr = inet_addr("42.192.83.143"); //点分十进制,字符串风格[0-255].[0-255].[0-255].[0-255]
// INADDR_ANY: 如果你bind的是确定的IP(主机), 意味着只有发到该IP主机上面的数据
// 才会交给你的网络进程, 但是,一般服务器可能有多张网卡,配置多个IP,我们需要的不是
// 某个IP上面的数据,我们需要的是,所有发送到该主机,发送到该端口的数据!
local.sin_addr.s_addr = INADDR_ANY;
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
std::cerr << "bind error : " << errno << std::endl;
return 2;
}
//3. 提供服务
bool quit = false;
#define NUM 1024
char buffer[NUM];
while(!quit)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
recvfrom(sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
std::cout << "client# " << buffer << std::endl;
std::string echo_hello = "hello";
sendto(sock, echo_hello.c_str(), echo_hello.size(), 0, (struct sockaddr*)&peer, len);
}
return 0;
}
- udp_client.cc
#include <iostream>
#include <string>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void Usage(std::string proc)
{
std::cout << "Usage: \n\t" << proc << " server_ip server_port" << std::endl;
}
// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return 0;
}
// 1. 创建套接字,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket error : " << errno << std::endl;
return 1;
}
//客户端需要显示的bind的吗??
// a. 首先,客户端必须也要有ip和port
// b. 但是,客户端不需要显示的bind!一旦显示bind,就必须明确,client要和哪一个port关联
// client指明的端口号,在client端一定会有吗??有可能被占用,被占用导致client无法使用
// server要的是port必须明确,而且不变,但client只要有就行!一般是由OS自动给你bind()
// 就是client正常发送数据的时候,OS会自动给你bind,采用的是随机端口的方式!
// b. 你要给谁发??
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t len = sizeof(server);
// 2.使用服务
while (1)
{
// a. 你的数据从哪里来??
std::string message;
std::cout << "输入# ";
std::cin >> message;
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
//此处tmp就是一个”占位符“
// struct sockaddr_in tmp;
// socklen_t len = sizeof(tmp);
char buffer[1024];
socklen_t len = sizeof(server);
recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&server, &len);
std::cout << "server echo# " << buffer << std::endl;
}
return 0;
}
如果上述文章对您有所帮助的话,还请点赞👍,收藏😉,关注🎈