前面学习了IPv4、IPv6、本地套接字的相关知识,现在学习一下怎么去使用前面所学的知识——使用对应套接字格式完成网络连接的建立
先有服务而后有客户
要创建一个套接字需要用到下面的函数
int socket(int domain, int type, int protocol)
由前面socket()
函数准备好的套接字如果要被使用,那么需要给它提供一个信号源,这就需要用到bind()
函数把套接字和套接字地址绑定,也就是将手机卡装入手机的SIM卡槽
bind(int fd, sockaddr * addr, socklen_t len)
如果是双卡双待手机,那该怎么绑定手机卡呢?
这个时候,可以利用通配地址的能力帮助我们解决这个问题
对IPv4地址来说,通配地址的设置通过INADDR_ANY来完成;对于IPv6地址来说,则是通过INADDR6_ANY来完成的
struct sockaddr_in name;
name.sin_addr.s_addr = htonl (INADDR_ANY); /* IPV4通配地址 */
除了设置地址外,还有一个参数就是端口号。如果把端口号设置为0,那么系统内核会按照一定的算法来选择一个空闲的端口,以完成socket的绑定(这在服务器端中并不常用,因为一般来说,服务器端需要绑定一个已知的地址和端口号,否则客户就不知道怎么联系上服务器)
下面是初始化IPv4 TCP socket的例子
#include
#include
#include
#include
int make_socket (uint16_t port)
{
int sock;
struct sockaddr_in name;
/* 创建字节流类型的IPV4 socket. */
sock = socket (PF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
perror ("socket");
exit (EXIT_FAILURE);
}
/* 绑定到port和ip. */
name.sin_family = AF_INET; /* IPV4 */
name.sin_port = htons (port); /* 指定端口 */
name.sin_addr.s_addr = htonl (INADDR_ANY); /* 通配地址 */
/* 把IPV4地址转换成通用地址格式,同时传递长度 */
if (bind (sock, (struct sockaddr *) &name, sizeof (name)) < 0)
{
perror ("bind");
exit (EXIT_FAILURE);
}
return sock
}
经过绑定电话卡后,一个正常可用的手机就诞生了,此时的手机都是自己准备好,以便可以使用的,这一系列过程都是主动性的,但是作为服务器端,需要“高冷”一点,不能太过主动,应该等待别人的主动。
listen()
将原本的主动性socket转化为被动性的socket,以告诉系统内核:“我现在只会等待别人的主动通知,我是用来被请求连接的!”
int listen (int socketfd, int backlog)
到这里,服务器端已经准备的差不多了,即将进入最后的等待模式
并发
:并发当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。.这种方式我们称之为并发(Concurrent)并行
:当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。上面的两个概念摘自并发,这里并行很好理解,但是并发里说的多个线程运行在一个时间段里同时运行
怎么理解呢?这个先记下来后面看明白了再回来补充
当客户端的连接请求到达时,服务器端应答成功,连接建立,这个时候操作系统内核需要把这个事件通知到应用程序,并让应用程序感知到这个连接。这个过程,就好比电信运营商完成了一次电话连接的建立, 应答方的电话铃声响起,通知有人拨打了号码,这个时候就需要拿起电话筒开始应答。
int accept(int listensockfd, struct sockaddr *cliaddr, socklen_t *addrlen)
listen()
转化后的被动socket为什么不把输入的套接字描述符和返回的描述符并为同一个?
这里就用到了上面所说到的并发
知识
和打电话的情形非常不一样的地方就在于,打电话一旦有一个连接建立,别人是不能再打进来的,只会得到语音播报:“您拨的电话正在通话中。”而网络程序的一个重要特征就是并发处理,不可能一个应用程序运行之后只能服务一个客户,如果是这样, 双 11 抢购得需要多少服务器才能满足全国 “剁手党 ” 的需求?所以监听套接字一直都存在,它是要为成千上万的客户来服务的,直到这个监听套接字关闭;而一旦一个客户和服务器连接成功,完成了 TCP 三次握手,操作系统内核就为这个客户生成一个已连接套接字,让应用服务器使用这个已连接套接字和客户进行通信处理。如果应用服务器完成了对这个客户的服务,比如一次网购下单,一次付款成功,那么关闭的就是已连接套接字,这样就完成了 TCP 连接的释放。请注意,这个时候释放的只是这一个客户连接,其它被服务的客户连接可能还存在。最重要的是,监听套接字一直都处于“监听”状态,等待新的客户请求到达并服务
第一步还是和服务端一样,要建立一个套接字,方法和前面是一样的。不一样的是客户端需要调用 connect 向服务端发起请求。
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
socket()
创建的套接字描述符connect()之前不需要bind()吗?
客户端在调用connect()
之前不一定需要调用bind()
函数,客户端作为主动方,发起的请求连接目标可以是多个,所以并不需要使用bind()
进行目标绑定。如果需要使用bind()
的话,内核会确定源IP地址,按照一定算法分配一个临时端口。
回想上一章节中的知识,(TCP socket)当程序执行到客户端的connect()
时,就已经触发了TCP中的三次握手机制了,而且仅在连接建立成功或者出错后才返回。连接建立成功返回很好理解,那什么是连接出错呢?
网上搜索到的简单讲解:
再以打电话为例子来浅读一下这几个知识点:(W:卧底,P:指挥官)