【Linux Network】网络编程套接字
目录
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. 端口号的认识
在计算机通信中,虽然看起来是各主机之间的通信,但其本质是两台主机上的两个进程之间的通
信,计算机网络只不过是两进程通信的临界资源,如何确定这两个进程呢?
我们使用IP地址可以确定相互通信的两台主机,但无法确定是主机上的哪个进程,这时我们便要使
用到端口号;
端口号:
端口号
(port)
是传输层协议的内容,
是一个2字节16位的整数,用来标识一个进程, 告诉操作系统当
前的这个数据要交给哪一个进程来处理;
总的来说:端口号:唯一的标识一台机器上的唯一的一个进程,并且
一个端口号只能被一个进程占
用;
源端口号和目的端口号:
传输层协议
(TCP
和
UDP)
的数据段中有两个端口号
,
分别叫做源端口号和目的端口号
.
就是在描述
"
数据是谁发的
,
要发给谁"(类似于源IP地址和目的IP地址);
3. 套接字socket
在一个计算机内部,我们可以使用 pid 来确定是哪两个进程进行通信;
在计算机网中,我们便要使用套接字来确定是哪两个进程相互通信;
套接字:IP(确定通信的两台主机)+端口号(确定主机上的进程);
pid表示唯一一个进程,此处端口号也是唯一确定一个进程,在这里使用端口号而不使用pid,主要
是为将网络和OS两个学科进行一个解耦,避免出现OS改变导致整个网络也要跟着变的情况;
在这里提一下:
一个进程可以绑定多个端口号
;
但是一个端口号不能被多个进程绑定,由此端口号和进程是1:1的
;
4. TCP协议和UDP协议
TCP协议:
此处我们先对
TCP(Transmission Control Protocol
传输控制协议
)
有一个直观的认识
;
后面我们再详细讨论
TCP
的一些细节问题.
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
UDP协议:
此处我们也是对
UDP(User Datagram Protocol
用户数据报协议
)
有一个直观的认识
;
后面再详细讨论
.
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
注意:
上述两种协议并无好坏之分,具体场景选择合适的协议;
5. 网络字节序
我们已经知道
,
内存中的多字节数据相对于内存地址有大端和小端之分;
磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分;
网络数据流同样有大端小端之分
.
那么如何定义网络数据流的地址呢
?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
总的来说:不管主机是大端存储还是小端存储,在网络数据流中,总是大端的;
为使网络程序具有可移植性
,
使同样的
C
代码在大端和小端计算机上编译后都能正常运行
,
可以调用
以下库函数做网络字节序和主机字节序的转换。
- 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
6. socket编程
sockaddr结构
socket API
是一层抽象的网络编程接口,
适用于各种底层网络协议,
如
IPv4
、
IPv6,
以及后面要讲
的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同;
- 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结构体指针做为参数;
sockaddr
结构
sockaddr_in
结构
虽然socket api
的接口是
sockaddr,
但是我们真正在基于
IPv4
编程时
,
使用的数据结构是
sockaddr_in; 这个结构里主要有三部分信息: 地址类型,
端口号
, IP
地址
.
in_addr
结构
in_addr用来表示一个IPv4
的
IP
地址
.
其实就是一个
32
位的整数;
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;
}
如果上述文章对您有所帮助的话,还请点赞👍,收藏😉,关注🎈