IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> 套接字编程 -> 正文阅读

[网络协议]套接字编程

1. 套接字的初步认识

套接字是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。
套接字是通信的基石,是支持TCP/IP协议的路通信的基本操作单元。

2.套接字的主要类型

流套接字(SOCK_STREAM)

流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。
流套接字使用tcp协议。

数据报套接字(SOCK_DGRAM)

数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。
数据报套接字使用udp协议。

原始套接字(SOCK_RAW)

原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接字。

3.udp套接字编程

(1)基本接口的认识

socket(创建套接字)

int socket(int domain, int type, int protocol);
参数:
domain:作用域,IPV4使用AF_INET,IPV6使用AF_INET6
type:套接字类型,udp使用SOCK_DGRAM,tcp使用SOCK_STREAM
protocol:使用的协议类型,UDP使用宏IPPROTO_UDP,tcp使用宏IPPROTO_TCP
返回值:成功返回套接字描述符,失败返回-1

bind(绑定地址信息)

int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数:
sockfd:套接字描述符
addr:要绑定的地址结构
IPV4通信程序需要使用sockaddr_in结构体,这个结构体必须为以下三个成员赋值才能成功绑定地址信息
sin_family:协议族,使用AF_INET
sin_addr.s_addr:ip地址
sin_port:端口号

addrlen:绑定的地址结构的大小
返回值:成功返回0,失败返回-1

recvfrom(接收数据)

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数:
sockfd:套接字描述符
buf:发送数据的首地址
len:发送数据的长度
flags:标识位,0默认阻塞发送
addr:对端地址结构,是一个输出型参数,不获取时设置为空
addrlen:接收的对端地址结构的大小,是一个输出型参数,不获取时设置为空
返回值:成功返回接收到的数据的字节数,失败返回-1

sendto(发送数据)

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默认阻塞发送
addr:对端地址结构
addrlen:对端地址结构的大小
返回值:成功返回发送的字节数,失败返回-1

close(关闭套接字)

int close(int fd);
参数:
fd:关闭的文件描述符
返回值:成功返回0,失败返回-1

(2)服务端的通信流程

创建套接字------->绑定地址信息-------->
接收数据------->发送数据------->关闭套接字
注意:服务端接收数据,发送数据是循环进行的,具体流程见下面的代码

(3)客户端的通信流程

创建套接字------->绑定地址信息(不推荐自己绑定,由操作系统帮我们绑定没有使用的地址)-------->
发送数据------->接收数据------->关闭套接字

(4)封装udp类进行网络通信

UdpSocket.hpp代码:

#include <iostream>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>


class UdpSocket{
	private:
		int _sockfd;
	public:
		UdpSocket()
			:_sockfd(-1)
		{}

		//创建套接字
		bool Socket()
		{
			_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
			if(_sockfd < 0)
			{
				perror("socket error");
				return false;
			}
			return true;
		}

		//绑定地址信息
		bool Bind(const std::string& ip,const uint16_t port)
		{
			//组织地址结构
			sockaddr_in addr;
			addr.sin_family = AF_INET;
			addr.sin_addr.s_addr=inet_addr(&ip[0]);
			addr.sin_port=htons(port);
			
			socklen_t len=sizeof(addr);
			int ret=bind(_sockfd, (sockaddr*)&addr, len);
			if(ret < 0)
			{
				perror("bind error");
				return false;
			}
			return true;
		}

		//接收数据
		//ip 和port 是输出型参数
		bool Recv(std::string& buf, std::string* ip=nullptr, uint16_t* port=nullptr)
		{
			sockaddr_in peerAddr;
			socklen_t len=sizeof(peerAddr);
			
			char tmp[4096]={0};
			int ret=recvfrom(_sockfd, tmp, 4095, 0, (sockaddr*)&peerAddr, &len);
			if(ret < 0)
			{
				perror("recvfrom error");
				return false;
			}

			buf.resize(ret);
			buf=tmp;
			
			//获得发送数据一方的ip和端口号
			if(ip != nullptr)
				*ip=inet_ntoa(peerAddr.sin_addr);
			if(port != nullptr)
				*port=ntohs(peerAddr.sin_port);

			return true;
		}
		//发送数据
		bool Send(const std::string& buf,const std::string& ip,const uint16_t port)
		{
			//组织地址结构
			sockaddr_in addr;
			addr.sin_family=AF_INET;
			addr.sin_addr.s_addr=inet_addr(ip.c_str());
			addr.sin_port=htons(port);
			
			int len=sizeof(addr);
			int ret=sendto(_sockfd, buf.c_str(), sizeof(buf), 0, (sockaddr*)&addr, len);
			if(ret < 0)
			{
				perror("Send error");
				return false;
			}
			return true;
		}
		//关闭套接字
		bool Close()
		{
			close(_sockfd);
			return true;
		}
};

服务端代码:

#include "UdpSocket.hpp"
#define CHECK_RET(q) {if(q == false) return -1;}

int main()
{
	UdpSocket sock;
	//创建套接字
	CHECK_RET(sock.Socket());
	//绑定地址信息
	CHECK_RET(sock.Bind("172.19.47.151",10000));
	while(1)
	{
		std::string buf;
		std::string ip;
		uint16_t port;
		//接收数据
		CHECK_RET(sock.Recv(buf, &ip, &port));
		std::cout<<"client ["<<ip<<":"<<port<<"]say "<<buf<<std::endl;
		
		buf.clear();
		std::cout<<"server say:";
		std::cin>>buf;
		//发送数据
		CHECK_RET(sock.Send(buf, ip, port));
	}
	//关闭套接字
	CHECK_RET(sock.Close());
}

客户端代码:

#include "UdpSocket.hpp"
#define CHECK_RET(q) {if(q == false) return -1;}
int main()
{
	//创建套接字
	UdpSocket sock;
	CHECK_RET(sock.Socket());
	while(1)
	{
		std::string buf;
		std::cout<<"client say: ";
		std::cin>>buf;

		//发送数据
		CHECK_RET(sock.Send(buf, "172.19.47.151", 10000));
		//接收数据
		buf.clear();
		CHECK_RET(sock.Recv(buf, nullptr, nullptr));
		std::cout<<"server say: "<<buf<<std::endl;
	}
	//关闭套接字
	CHECK_RET(sock.Close());
	return 0;
}

4.tcp套接字编程

(1)基本接口的认识

socket(创建套接字)

int socket(int domain, int type, int protocol);
参数:
domain:作用域,IPV4使用AF_INET,IPV6使用AF_INET6
type:套接字类型,udp使用SOCK_DGRAM,tcp使用SOCK_STREAM
protocol:使用的协议类型,UDP使用宏IPPROTO_UDP,tcp使用hongIPPROTO_TCP
返回值:成功返回套接字描述符,失败返回-1

bind(绑定地址信息)

int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数:
sockfd:套接字描述符
addr:要绑定的地址结构
IPV4通信程序需要使用sockaddr_in结构体,这个结构体必须为以下三个成员赋值才能成功绑定地址信息
sin_family:协议族,使用AF_INET
sin_addr.s_addr:ip地址
sin_port:端口号

addrlen:绑定的地址结构的大小
返回值:成功返回0,失败返回-1

connect(发起连接请求)

int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数:
sockfd:套接字描述符
addr:要连接的地址结构
addrlen:地址结构的大小
返回值:成功返回0,失败返回-1

listen(监听)

int listen(int sockfd, int backlog);
参数:
sockfd:要监听的套接字
backlog:同一时间最大监听数目
返回值:成功返回0,失败返回-1

accept(获取新建连接)

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
sockfd:监听套接字
addr:要连接的地址结构
addrlen:地址结构的大小
返回值:成功返回用于通信的套接字的文件描述符,失败返回-1

send(发送数据)

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数:
sockfd:套接字描述符
buf:发送数据的首地址
len:发送数据的大小
flags:发送方式(0表示阻塞发送)
返回值:成功返回发送的字符数,失败返回-1

recv(接收数据)

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:
sockfd:套接字描述符
buf:接收数据的首地址
len:接收数据的大小
flags:接收方式(0表示阻塞接收)
返回值:成功返回接收到的的字符数,失败返回-1

close(关闭套接字)

int close(int fd);
参数:
fd:关闭的文件描述符
返回值:成功返回0,失败返回-1

(2)服务端的通信流程

创建套接字----->绑定地址信息------->开始监听
------->获取新建连接----->接收数据------>发送数据----->关闭套接字
注意:服务端获取新建连接,接收数据,发送数据是循环进行的,具体流程见下面的代码

(3)客户端的通信流程

创建套接字----->绑定地址信息(不推荐自己绑定,由操作系统为我们绑定)------>发起连接请求
------>发送数据------->接收数据------>关闭套接字

(4)封装一个tcp类进行网络通信

tcpSocket.hpp代码:

#include<iostream>
#include<cstdio>
#include<string>
#include<unistd.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
#define LISTEN_BACKLOG 10
#define CHECK_RET(q) if((q) == false){return -1;}
class TcpSocket
{
private:
	int _sockfd;
public:
	TcpSocket():_sockfd(-1){}
	int getFd()
	{
		return _sockfd;
	}
	void setFd(int fd)
	{
		_sockfd=fd;
		return;
	}
	bool Socket()
	{
		//int socket(地址域类型,套接字类型,协议类型)
		//返回值:套接字描述符
		_sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
		if(_sockfd < 0)
		{
			perror("socket error");
			return false;
		}
		return true;
	}
	bool Bind(const std::string& ip,const uint16_t port)
	{
		sockaddr_in addr;
		addr.sin_family=AF_INET;//IPV4的地址结构
		addr.sin_addr.s_addr=inet_addr(&ip[0]);//将点分十进制的ip地址转换为网络上的ip地址
		addr.sin_port=htons(port);//将端口号转换为网络端口号

		//int bind(套接字描述符,地址信息,地址长度)
		int ret=bind(_sockfd,(sockaddr*)&addr,sizeof(addr));
		if(ret < 0)
		{
			perror("bind error");
			return false;
		}
		return true;
	}

	bool Listen(int backlog=LISTEN_BACKLOG)
	{
		//int listen(监听套接字,最大监听数)
		int ret=listen(_sockfd,backlog);
		if(ret < 0)
		{
			perror("listen error");
			return false;
		}
		return true;
	}

	bool Connect(const std::string& ip,const uint16_t port)
	{
		sockaddr_in addr;
		addr.sin_family=AF_INET;
		addr.sin_addr.s_addr=inet_addr(&ip[0]);
		addr.sin_port=htons(port);
		
		//int connect(套接字描述符,要连接的地址信息,地址长度)
		int ret=connect(_sockfd,(sockaddr*)&addr,sizeof(addr));
		if(ret < 0)
		{
			perror("connect error");
			return false;
		}
		return true;
	}
	
	//获取新建连接
	bool Accept(TcpSocket* sock,std::string* ip=NULL,uint16_t* port=NULL)
	{
		sockaddr_in addr;
		socklen_t len=sizeof(addr);
		
		//int accept(套接字描述符,地址信息,地址长度)
		int newfd=accept(_sockfd,(sockaddr*)&addr,&len);
		if(newfd < 0)
		{
			perror("accept error");
			return false;
		}

		sock->_sockfd=newfd;
		if(ip != NULL)
		{
			*ip=inet_ntoa(addr.sin_addr);
		}

		if(port != NULL)
		{
			*port=ntohs(addr.sin_port);
		}
		return true;
	}

	bool Recv(std::string* buf)
	{
		char tmp[4096]={0};
		//recv(套接字描述符,用来存放接收数据的缓冲区,接受的数据大小,接受方式(0表示阻塞接收))
		int ret=recv(_sockfd,tmp,4096,0);
		if(ret < 0)
		{
			perror("recv error");
			return false;
		}
		else if(ret == 0)
		{
			printf("peer shutdown");
			return false;
		}
		buf->assign(tmp,ret);//将tmp中ret个字节大小的数据存放到buf中
		return true;
	}

	bool Send(const std::string& data)
	{
		int total=0;
		while(total < data.size())
		{
			//send(套接字描述符,发送的数据的地址,发送数据的大小,发送方式(0表示阻塞发送))
			int ret=send(_sockfd,&data[0]+total,data.size()-total,0);
			if(ret < 0)
			{
				perror("send error");
				return false;
			}
			total+=ret;
		}
		return true;
	}

	bool Close()
	{
		if(_sockfd != -1)
			close(_sockfd);
		return true;
	}
};

服务端

#include"tcpSocket.hpp"

int main(int argc,char* argv[])
{
	if(argc != 3)
	{	
		std::cout<<"usage:./server ip port"<<std::endl;
		return -1;
	}
	std::string srvip=argv[1];
	uint16_t srvport=std::stoi(argv[2]);
	
	TcpSocket lst_sock;
	//1.创建套接字
	CHECK_RET(lst_sock.Socket());
	//2.绑定地址信息
	CHECK_RET(lst_sock.Bind(srvip,srvport));
	//3.开始监听
	CHECK_RET(lst_sock.Listen());
	while(1)
	{
		TcpSocket clisock;
		std::string cliip;
		uint16_t cliport;
		//4.获取新建连接
		bool ret=lst_sock.Accept(&clisock,&cliip,&cliport);
		//获取新建连接失败,继续获取
		if(ret  == false)
		{
			continue;
		}
		
		//5.接发数据
		std::cout<<"get newConn:"<<cliip<<":"<<cliport<<std::endl;
		std::string buf;
		ret=clisock.Recv(&buf);
		//接受数据失败,关闭套接字,创建新的套接字继续接收
		if(ret == false)
		{
			clisock.Close();
			continue;
		}
		std::cout<<"client say:"<<buf<<std::endl;

		buf.clear();
		std::cout<<"server say:";
		std::cin>>buf;
		ret=clisock.Send(buf);
		//发送失败,关闭套接字
		if(ret == false)
		{
			clisock.Close();
		}
	}
	//6.关闭套接字
	lst_sock.Close();
	return 0;
}

客户端

#include"tcpSocket.hpp"


int main(int argc,char* argv[])
{
	if(argc != 3)
	{
		std::cout<<"usage: ./client ip port"<<std::endl;
	}

	std::string ip=argv[1];
	uint16_t port=std::stoi(argv[2]);
	
	TcpSocket sock;
	//1.创建套接字
	CHECK_RET(sock.Socket());
	//绑定地址信息不推荐,因为操作系统会帮客户端分配ip和端口号
	//2.发起连接请求
	CHECK_RET(sock.Connect(ip,port));

	while(1)
	{
		//3.接发数据
		std::string buf;
		std::cout<<"client say:";
		std::cin>>buf;
		CHECK_RET(sock.Send(buf));

		buf.clear();
		CHECK_RET(sock.Recv(&buf));
		std::cout<<"server say:"<<buf<<std::endl;
	}
	//4.关闭套接字
	CHECK_RET(sock.Close());
	return 0;
}

当前程序运行起来存在的问题:

多个客户端与服务端通信时,服务端只能接收到客户端的一次数据。

原因及解决方案

问题出在服务端代码处,服务端循环获取新建连接、接收数据、发送数据。而这三个操作都是阻塞操作,当没有新的连接 请求到来时阻塞、当没有接收到数据时阻塞,当没有数据发送时阻塞。所以可以采用获取新建连接与接发数据分离的方式解决问题,有多进程和多线程两种解决方案

多进程服务端:
#include"tcpSocket.hpp"
#include<signal.h>
#include<sys/wait.h>
void worker(TcpSocket& clisock)
{
	bool ret;
	while(1)
	{
		std::string buf;
		ret=clisock.Recv(&buf);
		if(ret == false)
		{
			clisock.Close();
			exit(0);
		}
		std::cout<<"client say:"<<buf<<std::endl;

		buf.clear();
		std::cout<<"server say:";
		std::cin>>buf;
		ret=clisock.Send(buf);
		if(ret == false)
		{
			clisock.Close();
			exit(0);
		}
	}
	clisock.Close();
	exit(0);
}
int main(int argc,char* argv[])
{
	if(argc != 3)
	{	
		std::cout<<"usage:./server ip port"<<std::endl;
		return -1;
	}
	signal(SIGCHLD,SIG_IGN);
	std::string srvip=argv[1];
	uint16_t srvport=std::stoi(argv[2]);
	
	TcpSocket lst_sock;
	CHECK_RET(lst_sock.Socket());
	CHECK_RET(lst_sock.Bind(srvip,srvport));
	CHECK_RET(lst_sock.Listen());
	while(1)
	{
		TcpSocket clisock;
		std::string cliip;
		uint16_t cliport;
		bool ret=lst_sock.Accept(&clisock,&cliip,&cliport);
		if(ret  == false)
		{
			continue;
		}

		std::cout<<"get newConn:"<<cliip<<":"<<cliport<<std::endl;
		
		pid_t pid=fork();
		if(pid < 0)
		{
			clisock.Close();
			continue;
		}
		else if(pid == 0)
		{
			worker(clisock);
		}
		clisock.Close();//父子进程数据独有,释放父进程的资源
	}

	lst_sock.Close();
	return 0;
}
多线程服务端:
#include"tcpSocket.hpp"
#include<pthread.h>
void* thread_entry(void* arg)
{
	TcpSocket* clisock=(TcpSocket*)arg;
	while(1)
	{
		std::string buf;
		bool ret=clisock->Recv(&buf);
		if(ret == false)
		{
			clisock->Close();
			delete clisock;
			return NULL;
		}
		std::cout<<"client say:"<<buf<<std::endl;
		buf.clear();
		std::cout<<"server say:";
		std::cin>>buf;
		ret=clisock->Send(buf);
		if(ret == false)
		{
			clisock->Close();
			delete clisock;
			return NULL;
		}
	}
	clisock->Close();
	return NULL;
}
int main(int argc,char* argv[])
{
	if(argc != 3)
	{	
		std::cout<<"usage:./server ip port"<<std::endl;
		return -1;
	}
	std::string srvip=argv[1];
	uint16_t srvport=std::stoi(argv[2]);
	
	TcpSocket lst_sock;
	CHECK_RET(lst_sock.Socket());
	CHECK_RET(lst_sock.Bind(srvip,srvport));
	CHECK_RET(lst_sock.Listen());
	while(1)
	{
		TcpSocket* clisock=new TcpSocket();
		std::string cliip;
		uint16_t cliport;
		bool ret=lst_sock.Accept(clisock,&cliip,&cliport);
		if(ret  == false)
		{
			continue;
		}

		std::cout<<"get newConn:"<<cliip<<":"<<cliport<<std::endl;
		//子线程负责收发数据
		pthread_t tid;
		pthread_create(&tid,NULL,thread_entry,(void*)clisock);
		pthread_detach(tid);
	}

	lst_sock.Close();
	return 0;
}
  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-07-17 12:17:20  更:2021-07-17 12:18:50 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/8 10:05:47-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码