一.什么是Socket(套接字)
定义:就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。
定义比较难理解,总之我们认为通过socket及各层协议我们可以实现网络通信。网络通信的本质就是不同主机上进程的通信,那么我们是如何通过socket定位到一个主机的进程的呢?答案是ip(定位网络上唯一台主机)+端口号(主机上唯一一个进程),通过socket绑定ip和端口号,我们可以定位网络上唯一一个进程。
二.认识TCP协议和UDP协议
TCP:
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793 [1]定义。
TCP旨在适应支持多网络应用的分层协议层次结构。 连接到不同但互连的计算机通信网络的主计算机中的成对进程之间依靠TCP提供可靠的通信服务。TCP假设它可以从较低级别的协议获得简单的,可能不可靠的数据报服务。 原则上,TCP应该能够在从硬线连接到分组交换或电路交换网络的各种通信系统之上操作。
我们主要理解以下TCP协议四个特点:
- 传输层协议
- 有连接
- 可靠传输
- 面向字节
UDP:
此外UDP协议也是传输层常用的协议,UDP是User Datagram Protocol的简称,中文名是用户数据报协议,是OSI参考模型中的传输层协议,它是一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。
具有以下四个特定:
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
对于UDP本文不主要讲解
三. TCP中socket常用API 的讲解
1.sockaddr 结构
socket API 是一层抽象的网络编程接口,适用于各种底层网络协议,如 IPv4、IPv6,以及后面要讲的 UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同。简单了解就是,由于socket不仅能用于网络通信还能用于本地通信等,描述socket的地址格式应该不同。
主要有以下俩种格式:
第一种用于网络,第二种用于本地通信。
socket的地址在socket的多种API中我们都会用到,为了程序的通用性, 可以接收 IPv4, IPv6, 以及 UNIX Domain Socket 各种类型的 sockaddr 结构体指针做为参数, 因此我们采用一个 struct sockaddr *类型来接受各种参数(就类似于c++中的多态)。表示如下:
我们来了解网络通信需要的struct sockaddr_in的各种字段:
struct sockaddr_in {
short sin_family; // 2 字节 ,地址族,e.g. AF_INET, AF_INET6
unsigned short sin_port; // 2 字节 ,16位TCP/UDP 端口号 e.g. htons(3490),
struct in_addr sin_addr; // 4 字节 ,32位IP地址
char sin_zero[8]; // 8 字节 ,不使用
};
- sin_family主要用来定义是哪种地址族(地址族就是一个协议族所使用的地址集合,也是用宏来表示不同的地址族,这个宏的形式是AF开头,比如IP地址族为AF_INET,AF的意思是ADDRESS FAMILY)
- sin_port主要用来保存端口号
- sin_addr主要用来保存IP地址信息
- sin_zero没有特殊含义
struct in_addr的内容如下:
struct in_addr {
unsigned long s_addr; // 32位IPV4地址打印的时候可以调用inet_ntoa()函数将其转换为char *类型.
};
可以看出struct in-addr是用于存储IP的。
这里我们需要注意网络通信中的字节序是大端字节序,我们传入的ip和端口号需要从主机序列转换为网络序列。
2.socket函数(创建socket)
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
- 1.domain 参数: domain 参数指定套接字的协议族,常见的协议族有 AF_INET(IPv4)、AF_INET6(IPv6)和 AF_UNIX(本地套接字)等。前2个用于网络通信,最后一个用于本地通信。
- 2.type 参数: type 参数指定套接字的类型,常见的类型有 SOCK_STREAM(流套接字/TCP 套接字)和 SOCK_DGRAM(数据报套接字/UDP套接字)等。
- 3.protocol 参数:protocol 参数指定使用的协议,常见的协议有 IPPROTO_TCP(TCP 协议)和 IPPROTO_UDP(UDP 协议)等。如果指定为 0,则会根据 domain 和 type 参数自动选择协议。
- 4.返回值:socket()打开一个网络通讯端口,如果成功的话,就像 open()一样返回一个文件描述符;
3.bind函数(绑定ip和端口号)
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
bind()函数的作用是将ip和端口号绑定到socket()函数创建的文件的文件描述符上。
在服务端需要我们手动绑定,客户端操作系统会自动帮我们绑定
第一个参数sockfd是我们需要绑定的文件描述符。第二个是地址,第三个是地址的长度。
bind()成功返回 0,失败返回-1。
4. listen函数(监听连接请求)
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
第一个是socket文件描述符.
第二个参数backlog是侦听队列的长度。 在进程正在处理一个连接请求的时候,可能还存在其它的连接请求。因为TCP连接是一个过程,所以可能存在一种半连接的状态,有时由于同时尝试连接的用户过多,使得服务器进程无法快速地完成连接请求。如果这个情况出现了,服务器进程希望内核如何处理呢?内核会在自己的进程空间里维护一个队列以跟踪这些完成的连接但服务器进程还没有接手处理的连接(还没有调用accept函数的连接),这样的一个队列内核不可能让其任意大,所以必须有一个大小的上限。这个backlog告诉内核使用这个数值作为上限。
listen()成功返回 0,失败返回-1
5.accept函数(接受连接函数)
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
第一个参数还是表示文件描述符
第二个参数addr 是一个传出参数,accept()返回时传出客户端的地址和端口号, 如果给 addr 参数传 NULL,表示不关心客户端的地址;
第三个参数addrlen 参数是一个传入传出参数(value-result argument), 传入的是调用者提 供的, 缓冲区 addr 的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实 际长度(有可能没有占满调用者提供的缓冲区);
返回值是一个新的文件描述符,用新的文件描述符来进行信息的传输和接收。我们可以看做拉客的过程,第一个文件描述符主要用来门外拉客(监听和接受),第二个用于店内一对一服务客人(信息的传输和接收)
6.connect函数(连接函数客户端使用)
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
客户端需要调用 connect()连接服务器;
connect 和 bind 的参数形式一致, 区别在于 bind 的参数是自己的地址, 而 connect 的参数是对方的地址;
connect()成功返回 0,出错返回-1。
7.流程总结:
客户端:socket()---->accetp()(操作系统会自动调用bind())。
服务器端:socket()---->bind()---->listen()---->asscept()。