【计算机网络】认识网络套接字

套接字是一个抽象的概念,应用程序可以通过它进行收发数据,它可以像文件一样进行打开,读写和关闭的操作 。套接字允许应用层程序将I/O插入到网络中,并与网络的其他应用程序程序进行通信。
大家都知道,传输层可以实现端到端的通信,因此每一个传输层连接有两个端点,那么传输层连接的端点是什么呢?不是主机也不是主机的IP地址,不是应用进程也不是传输层协议的端口号,而是我们今天要认识的套接字。

套接字

上面讲到的,传输层间连接的端点就是套接字。根据RFC793的定义:它是由端口号拼接到IP地址构成 的。
即所谓的套接字,就是通过TCP通信的端点,每个套接字都有一个套接字序列包括主机IP和16位主机的端口号。
在通信时,其中一个网络应用程序将要传输的信息写入本主机的Socket中,该Socket通过网络接口卡的传输介质,将这段信息发送给另一台主机的Socket中,从而实现两个应用程序间进行数据传输。

这里,我么引出了三个概念:TCP,IP,端口号。下面我们依次认识一下它们

端口号

端口号(port)是传输层协议的内容,它有以下特点:

  • 端口号是一个2字节16位的整数
  • 端口号用来标识一个网络进程 ,告诉操作系统,当前这个数据要交给哪一个进程来处理
  • ip加端口号能标识我们网络中某个主机上的唯一一个进程 ,ip加端口号也就构成了套接字的概念
  • 一个端口号只能被一个进程所占用,但一个进程可以绑定多个端口号

看了上面的概念,我们知道端口号用于标识某台主机的一个进程,那么问题来了,它和之前学到的进程id有什么区别呢?
相信认真看了上面概念的同学隐约会发现它们的区别:一个进程只对应的一个id,但是可以对应多个端口号。
这怎么理解呢?那么你试着理解一下下面的例子:

你现在已经有一个可以唯一标识你身份的信息,就是你的身份证号(ID)。
后来你考上了大学,学校给你分配了学号0101用于标识你(端口号)。但是你不学无术被退学。
可你经过努力再次考入这所大学,此时学校分配学号0202用于标识你(另一端口号)。

此时再理解用于进程的ID和端口号的区别就不难了。这里端口号方便在网络中找到某个进程,而进程ID方便OS找到某个进程。

TCP和UDP

二者都是传输层协议,他们的区别如下:

TCP协议UDP协议
面向链接无链接
可靠传输不可靠传输
面向字节流面向数据报

这里关于TCP先简单介绍,你只需要知道它是面向链接的,其他的内容我在后面博客会详细讲到。
那么什么是链接呢?通俗点讲,链接就好比你和你男朋友达成约定成为男女朋友,那你脑海里就有了一个用于描述他的结构体,你们只要不分手,你就认为他(结构体)一直是你对象。这其实就是一种链接。UDP协议中不会进行描述所以就称无链接。
用TCP通信建立连接的过程,就是为通信双方构建描述双方信息的结构体

IP

IP地址全称互联网协议地址,是分配给用户上网使用的网际协议的设备的数字标签。
它提供了一种统一的地址格式,为互联网上每一个网络的每一个主机分配一个逻辑地址,以此来标识网络中的主机。

套接字编程

套接字编程是为了让两个不同网络上的主机实现通信,本质是让两个主机间的进程进行通信。

这里又有个问题,既然是两个网络上的主机,那么不同主机内存中的多字节数据相对于内存地址就有大端和小端之分,而网络数据流同样有大小端之分。那么如何定义网络数据流的地址才能保证正确的通信呢?
那么如何解决这些问题呢?与其从我们接收方俩端进行数据格式的重复转换,不如我们直接定义一个网络传输字符的规定,以后大家都用统一的大小端模式进行首发数据不就行了,所以这里就出现了我们的网络字节序 的概念。Tcp/Ip协议通一规定网络数据流采用大端字节序,也就是低地址高字节。

网络字节序
  1. 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
  2. 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
  3. 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据
  4. 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。下面函数看起来复杂,但并不难记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
在这里插入图片描述

Socket 编程接口

我在介绍套接字的时候说到,套接字可以像文件一样操作,其实是有依据的:因为创建套接字函数返回的就是一个文件描述符!!这与我们使用管道通信的原理相同,所以我们又说主机的通信就是进程间的通信。
下面从用户和内核角度分别理解网络套接字:

  1. socket是实现网络通信主机进程间通信的一种机制。从用户空间来看,socket就是一个文件描述符,对socket的操作等同于对普通文件的文件描述符进行操作,也就是说我们可以使用read,write,close等函数进行操作。一旦双方对该socket都进行初始化后,对端的数据交互都是通过该socket实现的。
  2. 而从内核空间来看,socket不在简单的指向一个磁盘文件,相应的读写指针指向的代码亦是网卡驱动程序提供的数据发送或者数据接收函数。其主要资源是一个内核内存空间的struct sk_buff结构体对象。该结构体中描述了双方的基本信息,缓冲数据等。

现在对Socket 编程有了更深的理解后,我们再来看相关接口:这些接口隔离了下层的实现,我们直接调用即可。
在这里插入图片描述

sockaddr 结构

接触过ip地址的小伙伴都知道ipv4标准的地址是32位,ipv6的ip地址是128位,然而这里的结构体指针居然只有这一种,那如果我使用不同协议怎么处理呢?不用担心,聪明的编写者使用了泛型编程的思想,即使传入的结构体类型可能会不同,但是只需要将对象转换成参数二类型的指针,他在底层自动帮你转换。那么怎么实现的呢,一起来看张图:下图中传入时的指针都是相同的,但是传入后会根据16位地址类型进行区分转换
在这里插入图片描述
sockaddr结构也就如我们下图所示,common字段中我们需要填写sin_family,这里我们填写AF_INET(表示采用IP地址)。
在这里插入图片描述
sockaddr_in 结构如下
在这里插入图片描述
in_addr 结构如下
在这里插入图片描述
这里大家可能好奇,结构体中的ip地址是int类型的,但是我们看到的ip地址通常都是"127.0.0.1"类似的形式,也就意味着一定有一组函数可以实现int类型和点分十进制ip转换,下面的函数正好完成:

  1. 字符串转in_addr函数:
    这三个函数中随便挑一个就行,第二个是最简单使用的,直接把字符串丢给他就行
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
int inet_pton(int af, const char *src, void *dst);//参数一填协议簇

  1. in_addr 转字符串函数
    推荐使用第二个,虽然第一个也很好用但函数是线程不安全的
char *inet_ntoa(struct in_addr in);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);//参数34分别是字符缓冲区和大小

常见问题

如何解决端口被占用问题

问题的描述:一台服务器为某个进程绑定了一个端口号,但是由于某种原因这个服务器宕机了,因此我们再次使用这个端口重启服务器时会报错绑定失败,因为Tcp的四次挥手时还有一个time wait状态,也就意味着此时端口还在被占用,所以就会出现无法再次用这个端口号绑定。

解决方法:
在这里插入图片描述
这里参数一为要设置的套接字对象
参数二默认设置为SOL_SOCKET,文档中好像只告诉了这一个选项
参数三就是你要设置什么选项,我们这里要设置的是SO_REUSEADDR,
选项描述为:指定用于验证bind()提供的地址时使用的规则应允许重用本地地址。
此时我们就能让刚才关闭的客户端可以直接再次重启。

如何随机绑定端口

客户端不用绑定端口,因为多个客户端绑定同一个端口时会出现绑定错误,就算没有错误也没什么意义,毕竟没有谁会主动连接客户端,所以只要操作系统随机给他一个就好。这里的随机与rand不同,仍然需要使用一些算法来计算才能给他分配。
设置随机绑定时,我们只需要把存放信息结构体中的sin_port选项设置为0,操作系统就会自动分配一个合适的端口号给客户端,并保证这个端口号没有被占用。

如何随机绑定ip

我们知道既然port可以不显式的绑定,那么ip一定也有办法:INADDR_ANY
这个宏的值其实也是0,所以讲到计算机中的随机值一定要想到数字零。不过这里操作系统“自动绑定”ip地址时一般就是你主机的ip地址,不然客户端怎么通过ip连你呢?
我们前面说一般要把端口号转为网络序列,所以使用htonl,不过这个宏是0的话大小端也就不那么重要了。

理解listen 的第二个参数

先记住listen的第二个参数+1 就是当前服务器能够建立全连接的队列的长度。
这是因为,Linux内核协议栈为一个tcp连接管理两个队列:处于SYN_ 的半连接和处于ESTABLISHED的全连接队列,当全连接队列满时就无法继续进入established状态了。
看下图应该就能理解了~
在这里插入图片描述

使用SOCK_DGRAM封装UdpSocket
#pragma once
#include <stdio.h>
#include <string.h>
#include <cassert>
#include <string>

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

typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;

class UdpSocket{
	public:
		UdpSocket():_fd(-1)
	{}
		//创建套接字
		bool Socket()
		{
			_fd=socket(AF_INET,SOCK_DGRAM,0);
			if(_fd<0)
			{
				perror("socket");
				return false;
			}
			return true;
		}

		bool Close()
		{
			close(_fd);
			return true;
		}

		//文件信息与网络信息绑定
		bool Bind(const std::string& ip,uint16_t port)
		{
			sockaddr_in addr;
			addr.sin_family=AF_INET; //公共字段
			addr.sin_addr.s_addr=inet_addr(ip.c_str());  //IP地址转换,字符串转in_addr函数
			addr.sin_port=htons(port);  //端口号,转换字节序
			int ret=bind(_fd,(sockaddr*)&addr,sizeof(addr));
			if(ret<0)
			{
				perror("bind");
				return false;
			}
			return true;
		}

		//读取信息
		bool RecvFrom(std::string* buf,std::string* ip=NULL,uint16_t* port=NULL)
		{
			char tmp[1024*10]={0}; //缓冲区
			sockaddr_in peer;
			socklen_t len=sizeof(peer);
			ssize_t r=recvfrom(_fd,tmp,sizeof(tmp)-1,0,(sockaddr*)&peer,&len);
			if(r<0)
			{
				perror("recvfrom");
				return false;
			}
			buf->assign(tmp,r);
			if(ip!=NULL)
			{
				*ip=inet_ntoa(peer.sin_addr);
			}
			if(port!=NULL)
			{
				*port=ntohs(peer.sin_port);
			}
			return true;
		}

		//发送信息
		bool SendTo(const std::string &buf,const std::string& ip,uint16_t port)
		{
			sockaddr_in addr;
			addr.sin_family=AF_INET;
			addr.sin_port=htons(port);
			addr.sin_addr.s_addr=inet_addr(ip.c_str());
			ssize_t w=sendto(_fd,buf.data(),buf.size(),0,(sockaddr*)& addr,sizeof(addr));
			if(w<0)
			{
				perror("sendto");
				return false;
			}
			return true;
		}

	private:
		int _fd;
};

使用SOCK_STREAM封装TcpSocket
#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <string>
#include <cassert>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;

#define CHECK_RET(exp) if(!exp){\
return false;\
}

class TcpSocket{
	public:
		TcpSocket():_fd(-1)
	{}
		TcpSocket(int fd):_fd(fd)
	{}
		bool Socket()
		{
			_fd=socket(AF_INET,SOCK_STREAM,0);
			if(_fd<0)
			{
				perror("socket");
				return false;
			}
			printf("open fd=%d\n",_fd);
			return true;
		}

		bool Close()const
		{
			close(_fd);
			printf("close fd=%d\n",_fd);
			return true;
		}

		bool Bind(const std::string &ip,uint16_t port)const
		{
			sockaddr_in addr;
			addr.sin_family=AF_INET;
			addr.sin_port=htons(port);
			addr.sin_addr.s_addr=inet_addr(ip.c_str());
			int ret=bind(_fd,(sockaddr*)&addr,sizeof(addr));
			if(ret<0)
			{
				perror("bind");
				return false;
			}
			return true;
		}

		bool Listen(int num)const
		{
			int ret=listen(_fd,num);
			if(ret<0)
			{
				perror("listen");
				return false;
			}
			return true;
		}

		bool Accept(TcpSocket* peer,std::string *ip=NULL,uint16_t *port=NULL)const
		{
			sockaddr_in peer_addr;
			socklen_t len=sizeof(peer_addr);
			int new_sock=accept(_fd,(sockaddr*)&peer_addr,&len);
			if(new_sock<0)
			{
				perror("listen");
				return false;
			}
			printf("accept fd=%d\n",new_sock);
			peer->_fd=new_sock;
			if(ip!=NULL)
				*ip=inet_ntoa(peer_addr.sin_addr);
			if(port!=NULL)
				*port=ntohs(peer_addr.sin_port);
			return true;
		}

		bool Recv(std::string *buf)const
		{
			buf->clear();
			char tmp[1024*10]={0};
			ssize_t r=recv(_fd,tmp,sizeof(tmp),0);
			if(r<0)
			{
				perror("recv");
				return false;
			}
			if(r==0)
			{
				return false;
			}
			buf->assign(tmp,r);
			return true;
		}

		bool Send(const std::string& buf)const
		{
			ssize_t w=send(_fd,buf.data(),buf.size(),0);
			if(w<0)
			{
				perror("send");
				return false;
			}
			return true;
		}

		bool Connect(const std::string &ip,uint16_t port)const
		{
			sockaddr_in addr;
			addr.sin_family=AF_INET;
			addr.sin_port=htons(port);
			addr.sin_addr.s_addr=inet_addr(ip.c_str());
			int ret=connect(_fd,(sockaddr*)&addr,sizeof(addr));
			if(ret<0)
			{
				perror("connect");
				return false;
			}
			return true;
		}

		int GetFd()const
		{
			return _fd;
		}
	private:
		int _fd;
};

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值