- 网络层的IP地址可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样可以利用三元组(
IP地址,协议,端口
)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志(socket
)与其它进程进行交互- 网络中的进程通过
socket
进行通信,那么什么是socket
呢?socket
其实就是一种特殊的文件,一些socket函数
即是对其进行的操作(读/写、打开、关闭)- 就目前而言,几乎所有的应用程序都是采用socket
下面就来简单了解一下在Tcp协议通信的socket
TCP交互流程
上述展示的交互流程,具体如下:
- 1.服务器根据地址类型(ipv4,ipv6)、socket类型、协议创建socket
- 2.服务器为socket绑定IP地址和端口号
- 3.服务器socket监听端口号请求,随时准备接收客户端发来的请求,这时候服务器socket并没有打开
- 4.客户端创建socket
- 5.客户端打开socket,根据服务器IP地址和端口号试图连接服务器socket
- 6.服务器socket接收到客户端socket请求,被动打开,开始接受客户端请求,直到客户端返回连接信息。这时候socket进入阻塞状态,所谓阻塞即accept()方法一直到客户端返回连接信息后才返回,开始接收下一个客户端请求
- 7.客户端连接成功,向服务器发送连接状态信息
- 8.服务器accept方法返回,连接成功
- 9.客户端向socket写入信息
- 10.服务器读取信息
- 11.客户端关闭
- 12.服务器端关闭
可以看出,服务器端socket与客户端socket建立连接的部分其实就是TCP的三次握手
socket接口函数
- socket函数
int socket(int domain, int type, int protocol);
- socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符,它唯一标识一个socket
- 创建socket时,指定不同的参数创建不同的socket描述符,socket函数的3个参数:
- 1.domain:即协议域,又称为协议族。常用的协议族有:AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,unix域socket)、AF_ROUTE等。协议族决定了socket的地址类型
- 2.type:指定socket的类型。常用的socket类型有:SOCK_STREAM(Tcp协议)、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等
- 3.protocol:指定协议。常用的协议有:IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,分别对应Tcp传输协议、UDP传输协议、STCP传输协议、TIPC传输协议
protocol为0时,会自动选择type类型对应的默认协议
- 当调用一个socket函数用来创建一个socket时,返回的socket描述字它存在于协议族中,但没有一个确定的地址。可以利用bind()函数赋予给它一个地址,否则系统将在调用connect()、listen()时随机分配一个端口
- 每个进程在自己的进程空间里都有一个套接字描述符表,但是套接字数据结构都存放在操作系统的内核缓冲里
- bind函数
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- (1).sockfd:即socket描述字,它是通过socket()函数创建来唯一标识一个socket的。bind()函数是给这个描述字绑定一个名字
- (2).addr:一个const struct sockaddr*指针,指向要绑定给sockfd的协议地址,有以下几种:
ipv4对应的如下代码:
struct sockaddr_in {
sa_family_t sin_family; //address familry: AF_INET
in_port_t sin_port; //port in network byte order
struct in_addr sin_addr; //internet address
};
struct in_addr {
uint32_t s_addr; //address in network byte order
};
ipv6对应的代码:
struct sockaddr_in {
sa_family_t sin6_family; //AF_INET6
in_port_t sin6_port; //port number
uint32_t sin6_flowinfo //ipv6 flow information
struct in6_addr sin6_addr; //Ipv6 address
uint32_t sin6_scope_id; //scope id (new in 2.4)
};
struct in6_addr {
unsigned char s6_addr[16]; //Ipv6address
};
UNIX域对应的代码:
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; //AF_UNIX
char sun_path[UNIX_PATH_MAX]; //pathname
};
- (3).addrlen:对应的地址长度
- listen和connect函数
int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应的socket可以排队的最大连接个数。socket函数创建的socket默认是一个主动类型的,listen函数将其变为被动的,等待客户的连接请求
- connect函数的第一个参数为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接
- accept函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr*的指针,用于返回客户端的协议地址;第三个参数为协议地址的长度。如果accept成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的Tcp连接
- read和write函数
网络I/O操作有下面几组:
read()/write()
recv()/writev()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
最常用的则是write()和read(),read原型如下:
ssize_t read(int fd, void *buf, size_t count);
read()函数负责从fd中读取内容。当读取成功时,read()返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。三个参数分别表示(1)socket描述字fd (2)缓冲区buf (3)缓冲区长度count
write()原型:
ssize_t write(int fd, const void *buf, size_t count);
write函数将buf中的nbytes写入文件描述字fd成功时返回写的字节数。失败时返回-1,并设置errno变量,当我们向套接字文件描述符写时有两种可能:(1)write的返回值大于0,表示写了部分或者是全部的数据 (2)返回的值小于0,此时出现了错误。三个参数分别表示:(1)fd表示socket描述字 (2)buf表示缓冲区 (3)count表示缓冲区长度
- close函数
#include <unistd.h> //头文件
int close(int fd);
close一个TCP socket的默认行为时,会把该socket标记为已关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数
close操作只是使相应的socket描述字的引用计数-1,只有当引用计数为0时,才会触发TCP的客户端向服务器发送终止连接请求
--------------------------------------------get------------------------------------------------------
了解TCP交互流程
简单编写一个TCP server