以下是阿鲤对套接字编程的一些理解性总结,希望对大家有所帮助;若存在错误,请慷慨指出;
1:套接字编程预备知识
2:socket api(套接字编程接口)介绍
3:udp协议的客户端/服务端的介绍实现
4:tcp协议的客户端/服务端的介绍实现
一:套接字编程预备知识
1:ip地址
IP协议规定网络上所有的设备都必须有一个独一无二的IP地址,就好比是邮件上都必须注明收件人地址,邮递员才能将邮件送到。同理,每个IP信息包都必须包含有目的设备的IP地址,信息包才可以正确地送到目的地。同一设备不可以拥有多个IP地址,所有使用IP的网络设备至少有一个唯一的IP地址。换言之,可以分配多个IP地址给同一个网络设备,但是同一个IP地址却不能重复分配给两个或以上的网络设备。
目前使用最广泛的IP地址规则为IPV4,其使用32位二进制来规定,而截至2019年11月26日,全球所有43亿个IPv4地址已分配完毕,这意味着没有更多的IPv4地址可以分配给ISP和其他大型网络基础设施提供商;所以现在大部分上网均采用动态地址分配,即便如此依旧不够使用;虽然出现了IPV6,但是IPV6还在部署初期;
2:端口号
端口号是一个2字节16位的整数;端口号用来标识一个进程,告诉操作系统,当前这个数据要交给哪一个进程来处理;所以ip地址+端口号就可以标识出某一台主机上的某一个进程(程序);
注意:一个端口号只能被一个进程占用,而一个进程可以占用多个端口号
3:通信协议
概念:网络通信中,通信双方使用的数据格式约定
意义:若要实现网络互联,就需要订立网络通信协议标准,统一网络通信数据格式
组成:在网络通信中,每条数据都会包括一个五元组(原ip地址/源端口/目的ip地址/目的端口/协议)
4:协议分层
意义;在通信环境中按照提供的服务,协议,接口对环境进行分层(一种封装),这样上层并不需要关心下层的一个实现,直接可以使用;使用的更加灵活便捷;
优点:协议分层后,通信环境层次清晰,并且每一层的功能具体实现会变的简单化,更容易形成标准
方式:网络通信环境时异常复杂的,因此为了更加容易的去实现网络通信功能因此对整个通信环境进行分层
1:IOS七层模型:/应用层-》表示层-》会话层-》传输层-》网络层-》链路层-》物理层 (分层太多,不容易实现)
2:TCP/IP五层模型:应用层-》传输层-》网络层-》链路层-》物理层(比较常用)
应用层:负责应用程序之间的数据沟通;自定制协议;知名协议(HTTP/FTP/SSH协议)
传输层:负责端与端之间的数据沟通(端口与端口之间);封装端口信息(TCP/UDP协议)
网络层:负责地址管理与路由选择;(选择最优路径);(IP协议,路由器)
链路层:相邻设备之间的数据传输(两个网卡之间,通过mac地址地址标识)(以太网协议(Etherne)交换机)
物理层:负责光电信号的传输(以太网协议,集线器(比如双绞线的长度直径,等等))
5:网络通信传输流程
发送信息:应用层-》传输层-》网络层-》链路层-》物理层
接受信息:物理层-》链路层-》网络层-》传输层-》应用层
如下图,假设你要使用扣扣发送一个hello;
注意:每一层都会记录上层所使用的协议
6:网络字节序:
字节序:cpu在内存中对数据存储的顺序,并且主机字节序分别为大端字节序和小端字节序,
小端字节序:低地址存低位 eg:X86架构
大端字节序:低地址存高位 eg:MIPS架构
在网络通信中并不知道对端主机是大端还是小端因此两个不同主机字节的主机在进行通信时会造成数据二义,字节序对网络通信造成影响主要针对存储大于一个字节的类型(int16_t,int32_t,short,int,long,float,double);
为了避免因为主机字节序不同导致的数据二义性,因此规定网络通信使用统一字节标准,把这个统一的标准字节序称为网络字节序:大端字节序,所以在编写网络通信程序中,若是传输大于一个字节类型的数据,就需要考虑字节序的转换gong
注意:可使用联合体使用大小端的判断
字节序转换接口:
uint32_t htonl(uint32_t hostlong);//把32位主机字节序转换成网络字节序
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);//把16位网络字节序转换成主机字节序
二:socket api(套接字编程接口)介绍
1:客户端,服务端
客户端:主动发起请求的一端
服务端:被动接受请求的一端
注:因为客户端的ip地址不顾定,所以只能由客户端向服务端发送请求
2:网络编程流程(套接字编程流程)
创建套接字(使网卡与进程之间建立联系,在内核中创建socket结构体)
为套接字绑定地址信息(ip/port/domain(地址域))(socket中保存着信息)
客户端向服务端发送数据,客户端指定一下对端的地址,这时候socket就会将数据从绑定的地址发送出去
服务端接收数据:客户端发送的数据到达服务端主机后,服务端操作系统根据这个数据的地址信息决定将这个数据放到哪一个套接字的缓冲区中(数据由指定的程序处理),服务端通过创建套接字返回描述符,在内核中找到套接字结构体,进而从缓冲区中取出数据
关闭套接字:释放内核中套接字占据的资源
注意:sockt中保存着ip,port, domain,接收发送内存缓冲区等信息
3:网络编程接口
创建:int socket(int domain, int type, int protocol);
domain:地址域(AF_INET :ipv4)
type:套接字类型 SOCK_STREAM/流式 (有序可靠双向字节流tcp) SOCK_DGRAM/数据报(数据报udp)
protocol:0-默认协议;IPPROTO_TCP (6) IPPROTO_UDP (17)
返回值:文件描述符--套接字文件句柄
地址绑定: int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
sockfed:套接字操作句柄,描述符
my_addr:通用地址结构,可以通过其里面的my_addr->falmily来确定类型
my_addr.family (ip类型)
my_addr.sin_port (端口号)
my_addr.sin_addr.s_addr (ip地址)
addrlen:addr长度
发送数据: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:对端地址
addrlen:对端地址长度
接收数据: ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
sockfd:套接字描述符
buf:要接收的数据放到buf中
len: buf的大小
flags:默认0(阻塞)
src_addr:对端地址
addrlen:地址长度
返回值:实际接收的数据长度
关闭:int close(int socket);
三:udp协议的客户端/服务端的介绍实现
udp特点:用户数据报协议--无连接,不可靠,面向数据报--应用于视频传输(允许少量丢失,实时性高)
直接上代码,代码里面详细的解析
1:客户端代码:udp_cli.cpp
/*========================================================================================================
封装实现一个udpsocket类;向外提供更容易使用的udp接口来实现udp通信流程
这是客户端代码
========================================================================================================= */
#include<iostream>
#include<string>
#include<netinet/in.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
class UdpSocket
{
int m_sockfd;
public:
UdpSocket():
m_sockfd(-1)
{}
~UdpSocket()
{
Close();
}
bool Socket()//创建套接字
{
m_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//ipv4,流式传输,UDP协议
if(m_sockfd < 0)
{
std::cerr << "socket error\n";
}
return true;
}
bool Bind(const std::string &ip, uint16_t port)//绑定套接字
{
struct sockaddr_in addr;//对端点
addr.sin_family = AF_INET;//ipv4类型
addr.sin_port = htons(port);//对端端口,转化成网络字节序
addr.sin_addr.s_addr = inet_addr(ip.c_str());//ip
socklen_t len = sizeof(struct sockaddr_in);//ip长度
int ret = bind(m_sockfd, (struct sockaddr*)&addr, len);//绑定
if(ret < 0)
{
std::cerr << "bind erroe\n";
return false;
}
return true;
}
bool Recv(std::string &buf, std::string &ip, uint16_t &port)//接收信息
{
char tmp[4096];//缓冲区
struct sockaddr_in peeraddr;//对端点
socklen_t len = sizeof(peeraddr);//对端点类型大小
int ret = recvfrom(m_sockfd, tmp, 4096, 0, (struct sockaddr*)&peeraddr, &len);//接收 ,套接字描述符,缓冲区,缓冲区大侠, 阻塞, 对端地址,对端地址大小
if(ret < 0)
{
std::cerr << "recvfrom error\n";
return false;
}
buf.assign(tmp, ret);//给buf开辟指定ret长度的空间,并且拷贝tmp(ret返回的就是接受信息的长度)
port = ntohs(peeraddr.sin_port);// 将网络字节序转化成主机字节序
ip = inet_ntoa(peeraddr.sin_addr);/*inet_ntoa,将sockaddr_in.sin_addr正数ip地址转换成字符串类型的ip地址,但是其返回的是缓冲区首地址,
return true;
}
bool Send(std::string data, std::string ip, uint16_t port)//发送
{
struct sockaddr_in addr;//对端地址
addr.sin_family = AF_INET;//IPv4
addr.sin_port = htons(port);//对端端口
addr.sin_addr.s_addr = inet_addr(ip.c_str());//对端ip
socklen_t len = sizeof(struct sockaddr_in);
int ret = sendto(m_sockfd, &data[0], data.size(), 0, (struct sockaddr*)&addr, len);//发送:套接字操作句柄,发送到信息,发送信息大小,阻塞发送,对端端点,对端地址大小
if(ret < 0)
{
std::cerr << "sento error\n";
return -1;
}
return true;
}
bool Close()//关闭
{
if(m_sockfd >= 0)
{
close(m_sockfd);
m_sockfd = -1;
return true;
}
return false;
}
};
#define CHECK_RET(q) if((q) == false){return -1;}
int main(int argc, char *argv[])//通过主函数传参,直接写入
{
if(argc != 3)
{
std::cout << "plase scanf ./udp_cli ip port\n";
return -1;
}
UdpSocket sock;//定义一个套接字编程类
std::string srv_ip = argv[1];
uint16_t srv_port = atoi(argv[2]);
CHECK_RET(sock.Socket());//创建套接字
//客户端可以不不绑定 系统会分配合适的
//CHECK_RET(sock.Bind(argv[1], 8000));//套接字绑定
while(1)
{
std::string buf;
std::cin >> buf;
CHECK_RET(sock.Send(buf, srv_ip, srv_port));//发送
buf.clear();
CHECK_RET(sock.Recv(buf, srv_ip, srv_port));//接收
std::cout << "server say: " << buf << std::endl;
}
CHECK_RET(sock.Close());
return 0;
}
2:服务端代码 udp_srv.c
/*========================================================================================================
传输层基于udp协议的服务端程序
1:创建套接字
2:为套接字绑定地址信息
3:接收数据
4;发送数据
5:关闭套接字
========================================================================================================= */
#include<stdio.h>
#include<unistd.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<string.h>
#include<arpa/inet.h>
#include<stdlib.h>
int main(int argc, char *argv[2])
{
if(argc != 3)
{
printf("Usage: ./main: %s %s\n",argv[1], argv[2]);
return -1;
}
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//创建套接字 //ipv4 数据报类型 udp协议
if(sockfd < 0)
{
perror("socket error");
return -1;
}
struct sockaddr_in addr;//定义ipv地址结构
addr.sin_family = AF_INET;//地址结构类型
addr.sin_port = htons(atoi(argv[2]));//绑定端口
addr.sin_addr.s_addr = inet_addr(argv[1]);//将字符串点分十进制ip地址转换成网络字节序,绑定ip
socklen_t len = sizeof(struct sockaddr_in);//地址长度
int ret = bind(sockfd, (struct sockaddr*)&addr, len);//地址绑定 操作句柄 地址结构 地质结构大小
if(ret < 0)
{
perror("bind error");
return -1;
}
while(1)
{
//接收数据
char buf[1024] = {0};//缓冲区
struct sockaddr_in cliaddr;//对端地址结构
socklen_t len = sizeof(struct sockaddr_in);//对端地址大小
int ret = recvfrom(sockfd, buf, 1023, 0, (struct sockaddr*)&cliaddr, &len);//操作句柄,缓冲区,缓冲区大小,阻塞,地址,地址大小
if(ret < 0)
{
perror("recvfrom error");
close(sockfd);
return -1;
}
printf("client say: %s\n", buf);
//输出数据
len = sizeof(struct sockaddr_in);
memset(buf, 0x00, 1024);//清空缓冲区
scanf("%s", buf);
ret = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&cliaddr, len);//操作句柄,缓冲区,缓冲区大小,阻塞,对端地址
if(ret < 0)
{
perror("sendto error");
close(sockfd);
return -1;
}
}
close(sockfd);
return 0;
}
四:tcp协议的客户端/服务端的介绍实现
tcp协议特点:传输控制协议--面向连接,可靠传输,提供字节流传输服务--应用于文件纯属-文件/压缩包/程序(安全性高)
1:图解tcp流程
注:三次握手,指在建立链接过程中,客户端先向服务端发出syn请求,然后服务端对客户端进行ack回复及syn请求,客户端再进行ack请求,来回了三次才能确定可以建立链接(因为TCP协议是面向连接的,所以必须确定客户端和服务端均在线)
以上函数的实现及解析会在代码中实现介绍
直接上代码,代码里面详细的解析
阿鲤在这里直接给大家github的链接,里面有以下四种类型
tcp_cli.cpp | 客户端 |
tcp_srv | 服务端 |
tcpsocket.hpp | tcp封装头文件 |
thread_tcp.cpp | 多线程服务端 |
process_tcp.cpp | 多进程服务端 |
注意:因为tcp协议存在三次握手,在传输信息时会服务端每次都会新建立一个新的socket所以在多个客户端向服务端发出请求时,会覆盖原先的socket的操作句柄(fd),导致之前的客户端链接失效,所以需要多线程或多进程实现
来源:CSDN
作者:belongAL
链接:https://blog.csdn.net/belongHWL/article/details/104023636