网络基础和套接字编程
Posted DR5200
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络基础和套接字编程相关的知识,希望对你有一定的参考价值。
文章目录
在体系结构中,网络在什么位置?
用户使用电脑/手机上的app时,app会将用户的操作发送给操作系统,操作系统内有一个协议栈,协议栈将用户的数据进行封包,再通过网卡传递到网络中,网络内部经过各种路由转发,将数据传送到了目标服务器,服务器的协议栈将数据进行解包传递给服务器的应用层,服务器的应用层解析后再传递回用户(将上述过程逆过来)
协议栈是网络标准化组织定义的,所有的操作系统必须支持(TCP/IP协议)
网络协议栈用来完成数据通信工作
应用层 : 网络协议的开发人员在用户空间开发出各种常用协议(http协议,FTP协议)
传输层(由操作系统维护的) : tcp协议
网络层(由操作系统维护的) : ip协议
数据链路层 : 在网卡驱动中,与驱动程序强相关,负责真正的数据传输
各个层的功能 :
应用层 : 根据特定的通信的目的,进行数据分析与处理达到某种业务性的目的
传输层 : 处理传输遇到的问题,主要是保证数据可靠性
网络层 : 数据转发,解决数据会被发送到哪里的问题
数据链路层 : 负责真正的数据发送过程
层状结构本质 : 实现软件工程上面的解耦,层与层之间只有接口的互相调用关系,增加代码的可维护性和扩展性,分层可以隐藏底层细节,实现封装
路由器(实现了从网络层到物理层,有些也实现了部分传输层的内容(如端口转发)) : 路由转发,链接不同的局域网
交换机(实现了从数据链路层到物理层,有些也实现了网络层的转发) : 数据转发,局域网内的数据转发
集线器 : 扩大电信号,能够把数据传送到更远
协议是通信计算机双方必须共同遵从的一组约定。如怎么样建立连接、怎么样互相识别等。只有遵守这个约定,计算机之间才能相互通信交流,而想让计算机实现网络通信,就需要用计算机的语言表达出协议且通信双方能够认识协议,如下图例子
我们可以用位段来表示某种协议的规定,mycmd就可以称做双方通信的报头/报文,1/2/3为通信双方约定的操作
计算机生产厂商有很多; 计算机操作系统, 也有很多; 计算机网络硬件设备, 还是有很多; 如何让这些不同厂商之间生产的计算机能够相互顺畅的通信呢? 就需要有人站出来, 约定一个共同的标准, 大家都来遵守, 这就是网络协议
关于通信,同层协议可以认为自己在和对方层直接进行通信,从而简化对于网络协议栈的理解
OSI(Open System Interconnection,开放系统互连)七层网络模型称为开放式系统互联参考模型,是一个逻辑上的定义和规范,把网络从逻辑上分为了7层. 每一层都有相关、相对应的物理设备,比如路由器,交换机,OSI 七层模型是一种框架性的设计方法,其最主要的功能使就是帮助不同类型的主机实现数据传输, 它的最大优点是将服务、接口和协议这三个概念明确地区分开来,概念清楚,理论也比较完整. 通过七个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯,但是, 它既复杂又不实用,所以我们按照TCP/IP四层模型来讲解
网络传输的基本流程
A主机的用户发送一条消息给B主机的用户,消息首先交给应用层,应用层添加上应用层的报头,接下来依次添加上传输层,网络层,数据链路层的报头(底层操作系统会提供一个内核缓冲区,各个层的报头会逐一拷贝到内核缓冲区中),然后网卡通过局域网发送给B主机,这一过程叫做自顶向下数据封包
B主机的网卡接受到数据,将数据向上进行交付,数据链路层识别该层的报头信息,将剩余部分(有效载荷)交给网络层,网络层识别该层的报头信息,将剩余部分交给传输层,传输层识别该层的报头信息,将剩余部分交给应用层,传输层识别该层的报头信息,将剩余部分交给B主机用户,这一过程叫做数据的解包和分用
当前层如何区分报头和有效载荷?
(1). 定长报头
(2). 自定义描述字段
当前层怎么知道将有效载荷交给上层的哪个协议呢?
协议共性:
(1). 协议报头的大小使用定长报头或自定义描述字段来确定
(2). 协议报头中都包含一个字段,表示要将有效载荷交付给上层哪个协议
(1). A主机通过局域网把数据发送给B主机的过程中,实际上局域网内所有的主机底层都收到了数据,只是判断该数据不是发送给自己的后,就不会对此数据做出反应,B主机判断该数据是发送给自己的后,对数据进行处理
(2). 局域网内的主机可能同时在给别的主机发送数据,会发生数据碰撞的问题,发生数据碰撞后,当前主机能够检测到并进行碰撞避免算法
数据链路层的数据通信过程
每一台计算机都至少有一个网卡,每个网卡都内置了48位的序列号,该序列号为MAC地址,MAC地址全球唯一
局域网中发送的数据称为MAC数据帧
A主机发送MAC数据帧到局域网中,所有主机都收到了A发送的MAC数据帧,将dstMAC和自己的MAC做对比,发现匹配不上将MAC数据帧丢弃,B主机发现匹配进行数据处理,这种情况叫做单向数据发送
我们可以将 dstMAC 设置成所有主机(一般设置成全F),该MAC数据帧就是发送给局域网内所有主机的(所有主机都可以接收),这种情况叫做广播信息
ifconfig 查看当前主机网卡信息
路由器至少能够横跨两个局域网,对于每个网络,都认为路由器是它的局域网上的一台主机
IP : 用来标识全网内唯一的一台主机
ipv4 : 32位比特位标识 ip 地址
我们常见的IP地址可能是这种类型 : 192.182.49.10,这种叫做点分十进制字符串风格IP地址
IP地址的每段可以看成是一个0-255的整数(8个比特位),把每段拆分成一个二进制形式组合起来,然后把这个二进制数转变成整数(整数风格IP)
ipv6 : 128位比特位标识 ip 地址
跨网络通信(A主机和1号主机) :
(1). A主机的信息自顶向下进行数据封包通过以太网将MAC数据帧发送给路由器,经过路由器网卡,数据链路层解包将数据交给网络层(通过目标IP判断发给哪个网络)
(2). 路由器给数据添加令牌环网协议报头(封包),通过令牌环网发送给1号主机,经过解包分用1号用户得到消息
通过跨网络通信我们可以发现A主机,路由器,B主机的应用层,传输层,网络层的数据都是一样的,所以我们通过IP层虚拟化了底层网络的差异(这种现象在虚拟地址空间和一切皆文件均有体现)
不同的协议层对数据包有不同的称谓,在传输层叫做段,在网络层叫做报,在链路层叫做帧
客户端打开浏览器向百度提交搜索请求,经过上述跨网络通信过程,将请求提交给百度服务器的搜索引擎服务,而浏览器和搜索引擎服务都是进程,所以后面要说的 socket 通信,本质上是进程通信,只不过是跨网络的进程间通信
(1). 客户端有可能打开多个进程,那么服务器端的服务信息传递回客户端后,怎么知道把服务信息传递给哪个进程呢?
(2). 服务器端有可能部署了多个服务,客户端的请求信息传递给服务器端后,怎么知道把请求信息传递给哪个服务呢?
任何的网络服务和客户端要进行正常的数据通信,必须用端口号来唯一标识自身,在同一个OS内,一个进程可以和一个端口号进行绑定,该端口号就可以在网络层面唯一标识一台主机上的一个进程
(1). 端口号(port)是传输层协议的内容.
(2). 端口号是一个2字节16位的整数;
(3). 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
(4). IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
(5). 一个端口号只能被一个进程占用
(6). pid vs 端口号(port) :
一台机器上,可能存在大量进程,但不是所有的进程都要对外进行网络请求,pid用来唯一标识一台机器上所有的进程,端口号(port)用来唯一标识一台机器上需要对外进行网络请求的进程(类比身份证号和学号)
TCP协议(传输控制协议)特点 :
(1). 传输层协议
(2). 面向连接
(3). 可靠传输
(4). 面向字节流
UDP协议(用户数据报协议)特点 :
(1). 传输层协议
(2). 无连接
(3). 不可靠传输
(4). 面向数据报
有了TCP协议,为什么还要有UDP协议呢?
保证可靠是需要我们做更多的工作的,所以TCP协议比UDP协议更简单,如果一个场景允许少量丢包的话,可以优先考虑UDP协议,如果一个场景不允许丢包,肯定要使用TCP协议
网络字节序
如果客户端是一台小端机,服务端是一台大端机,发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,但由于服务器是大端机就会导致读取到的数据和客户端发送的不同,所以TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节,不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据,如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
网络字节序和主机字节序之间的转化
#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); 网络转主机
struct sockaddr_in : 网络套接字,用来网络通信
struct sockaddr_un : 本地套接字,用于本地通信
struct socketaddr : 通用套接字结构
可以使用 grep -ER ‘struct sockaddr_in ’ /usr/include 查找该结构
grep -ER 'struct sockaddr_in ' /usr/include
/usr/include/linux/in.h:struct sockaddr_in
struct in_addr
uint32_t s_addr; // IPv4地址
;
struct sockaddr_in
sa_family_t sin_family; // AF_INET,套接字地址结构的地址族
uint32_t sin_port; // 端口号
struct in_addr sin_addr;
;
点分十进制IP和整数IP可以借助位段和联合体来转换,在网络传输中需要整数IP
点分十进制IP转整数IP : 将点分十进制IP写入位段中,从联合体中读取整数IP(uint32_t ip)出来即可
整数IP转点分十进制IP : 将整数IP写入联合体(uint32_t ip)中,从位段中读取点分十进制IP出来即可
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
将字符串IP转化成整形IP
in_addr_t inet_addr(const char *cp);
将整数IP转化成字符串IP
char *inet_ntoa(struct in_addr in);
UDP相关接口
#include <sys/types.h>
#include <sys/socket.h>
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
domain : 决定套接字是什么类型的(AF_INET(网络间通信)/AF_UNIX(本机通信))
type : 套接字创建时的服务类型
SOCK_STREAM : TCP,流式服务
SOCK_DGRAM : UDP,用户数据报服务
protocol : 协议类型,一般设置为0就可以
返回值 : 成功后返回一个文件描述符(打开网卡文件),失败返回-1
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
socket : 创建套接字返回的文件描述符
address : struct socketaddr 结构体
address_len : struct socketaddr 结构体的大小
返回值 : 成功返回0,失败返回-1
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
发送给特定套接字信息
sockfd : 创建套接字返回的文件描述符
buf : 要发送的信息
len : 发送信息的大小
flags : 读写方式,一般设置为0表示阻塞写
dest_addr : 对端的IP地址和端口号存放的地址(发送给谁?)
addrlen : dest_addr 的大小
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
从特定套接字中进行读取
sockfd : 创建套接字返回的文件描述符
buf : 读取内容存放的地址
len : 期望读取的字节数
flags : 读写方式,一般设置为0表示阻塞读
src_addr : 读取的对端的IP地址和端口号存放的地址
addrlen : 传入时为 src_addr 的大小,返回时代表 src_addr 具体的大小
返回值 : 实际读取的字节数,失败返回-1
UDP注意事项:
udp服务器调用了bind()函数为服务器套接字绑定本地地址/端口,这样使得客户端的能知道它发数据的目的地址/端口,服务器如果单单接收客户端的数据,或者先接收客户端的数据(此时通过recvfrom()函数获取到了客户端的地址信息/端口)再发送数据,客户端的套接字可以不绑定自身的地址/端口,因为udp在创建套接字后直接使用sendto(),隐含操作是,在发送数据之前操作系统会为该套接字随机分配一个合适的udp端口,将该套接字和本地地址信息绑定。(客户端端口不需要和客户端进程强相关)
但是,如果服务器程序就绪后一上来就要发送数据给客户端,那么服务器就需要知道客户端的地址信息和端口,那么就不能让客户端的地址信息和端口号由客户端所在操作系统分配,而是要在客户端程序指定了
云服务器的IP,是由对应的云厂商提供的,这个IP不能直接被绑定,如果需要bind,要bind 0(INADDR_ANY),意味着服务器可以接收任何客户端的请求
UDP服务器编写
all:udp_server udp_client
udp_client:udp_client.cc
g++ -o $@ $^ -std=c++11
udp_server:udp_server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f udp_client udp_server
udp_client.hpp
#include<iostream>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
class UdpClient
private:
int sock;
public:
UdpClient()
void InitClient()
sock = socket(AF_INET,SOCK_DGRAM,0);
if(sock < 0) exit(1);
void Start()
while(1)
std::cout<<"Please enter#";
std::string msg;
std::cin>>msg;
struct sockaddr_in peer;
peer.sin_family = AF_INET;
peer.sin_port = htons(4567);
peer.sin_addr.s_addr = inet_addr("127.0.0.1");
sendto(sock,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));
char buffer[1024] = 0;
socklen_t len = sizeof(peer);
ssize_t s = recvfrom(sock,buffer,sizeof(buffer),0,(struct sockaddr*)&peer,&len);
if(s > 0)
std::cout<<buffer<<std::endl;
~UdpClient()
if(sock > 0) close(sock);
;
udp_client.cc
#include"udp_client.hpp"
int main(int argc,char* argv[])
UdpClient* ucl = new UdpClient;
ucl->InitClient();
ucl->Start();:
return 0;
udp_server.hpp
#include<iostream>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
class UdpServer
private:
int sock;
public:
UdpServer()
void InitServer()
sock = socket(AF_INET,SOCK_DGRAM,0);
if(sock < 0) exit(1);
std::cout<<"socket create success"<<std::endl;
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(4567);
local.sin_addr.s_addr = INADDR_ANY;
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
exit(2);
std::cout<<"bind success"<<std::endl;
void Start()
while(1)
char buffer[1024] = 0;
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t s = recvfrom(sock,buffer,sizeof(buffer),0,(struct sockaddr*)&peer,&len);
if(s > 0)
std::cout<<"client#"<<buffer<<std::endl;
std::string msg = "server#";
msg += buffer;
sendto(sock,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,len);
~UdpServer()
if(sock > 0) close(sock);
;
udp_server.cc
#include"udp_server.hpp"
int main(int argc,char* argv[])
UdpServer* usr = new UdpServer;
usr->InitServer();
usr->Start();
return 0;
TCP相关接口
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
domain : 决定套接字是什么类型的(AF_INET(网络间通信)/AF_UNIX(本机通信))
type : 套接字创建时的服务类型
SOCK_STREAM : TCP,流式服务
SOCK_DGRAM : UDP,用户数据报服务
protocol : 协议类型,一般设置为0就可以
返回值 : 成功后返回一个文件描述符(打开网卡文件),失败返回-1
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
socket : 创建套接字返回的文件描述符
address : struct socketaddr 结构体
address_len : struct socketaddr 结构体的大小
返回值 : 成功返回0,失败返回-1
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
socket : 监听套接字
backlog : 全链接队列的最大长度
返回值 : 成功返回0,失败返回-1
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
返回值: 收发数据套接字
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
返回值: 成功返回0,失败返回-1
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
flags : 设置发送方式,设置为0为阻塞式发送
返回值 > 0 : 实际发送的字节数
返回值 = 0 : 说明对端关闭链接
返回值 < 0 : 说明写入出错
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
flags : 设置发送方式,设置为0为阻塞式接受
返回值 > 0 : 实际读取的字节数
返回值 = 0 : 说明对端关闭链接
返回值 < 0 : 说明读取出错
TCP服务器编写
tcp_server.hpp
#include<iostream>
#include<unistd.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/types.h>
#include<sys/socket.h>
class TcpServer
private:
int lsock;
public:
void InitServer()
lsock = socket(AF_INET,SOCK_STREAM,0);
if(lsock < 0) exit(1);
std::cout<<"socket success"<<std::endl;
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(4567);
local.sin_addr.s_addr = INADDR_ANY;
if(bind(lsock,(struct sockaddr*)&local,sizeof(local)) < 0) exit(2);
std::cout<<"bind success"<<std::endl;
if(listen(lsock,5) < 0) exit(3);
std::cout<<"listen success"<<std::endl;
void Start()
while(1)
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int cSock = accept(lsock,(struct sockaddr*)&peer,&len);
if(cSock < 0)
std::cout<<"accept error"<<stdTCP/IP网络编程:05TCP原理 --简单描述