socket接口介绍:
服务端与客户端的步骤不同的是客户端第三步为发送数据第四步为接收数据
五元组:源端ip,源端端口,对端ip,对端端口,协议
一条五元组可以标识一条通信
其中对端ip和对端端口是为了确定网络中的内一台主机和进程,网络中的数据通过对端ip发送给目标主机之后,再通过端口号确定需要发送到的进程
UDP通信
udp通信特点:无连接,不可靠,面向数据报
服务端:
- 1.创建套接字:在内核中创建socket结构体,为了与网卡建立联系,网络通信依靠网卡
- 2.为socket套接字绑定地址信息,socket描述的地址信息是服务器的地址和协议信息(因为是服务端,所以告诉客户端发往那个地址和端口,一旦收到发送到这个地址和端口的数据就会将这份数据放到对应socket的接收缓冲区中)
- 3.接收数据,从对应的socket的接收缓冲区中取出数据
- 4.发送数据,将数据写入内核中的socket中的发送缓冲区中,操作系统选择合适的时间将数封装并发送
- 5.关闭套接字
客户端:
- 1.创建套接字
- 2.为套接字绑定地址信息:描述在内核中创建的socket结构体的源端地址信息,发送的数据中的源端地址信息就是绑定的地址信息(不推荐主动绑定,可以交给系统自动分配,所以可以忽略这一步)
- 3.发送数据(此时若发现没有绑定地址信息,系统则主动选择一个合适的地址端口去绑定),将数据放到socket的发送缓冲区中,系统选择合适时间封装并发送
- 4.接收数据
- 5.关闭套接字
注意: - 为什么不推荐客户端主动绑定地址信息?
因为永远都是客户端向服务端发送请求 ,所以服务端必须提前告诉服务端的地址信息
还有可能是因为如果用户客户端非要自己绑定地址信息,如果这个地址(端口)被占用的话,此时绑定就会失败,所以一般不要主动绑定,交给系统判定
客户端不主动绑定地址端口是为了降低端口冲突的概率
服务端需要主动绑定地址端口,因为客户端必须知道服务端的地址端口,如果服务端不绑定地址端口,客户端就无法发送给服务端的请求
客户端必须知道服务端的地址才可以发送请求,一般来说,我们平常使用的客户端里面都包装好了服务端的地址信息
通信顺序
1 2 3
客户端->服务端->客户端
UDP编程接口介绍
**创建套接字/为套接字绑定地址信息/接收数据/发送数据/关闭套接字
- 1.创建套接字
在内核中创建socket结构体返回操作句柄
int socket(int domain, int type ,int protocol);
//domain:地址域-确定本次socket通信使用那种协议版本的地址结构-不同的协议版本有不同的地址结构 -- AF_INET IPV4网络协议
//type:套接字类型(流式套接字-SOCK_STREAM / 数据报套接字-SOCK_DGRAM)
//protocol:协议类型(通常就是传输层协议的选择 IPPROTO_TCP / IPPROTO_UDP),默认为0-流式默认tcp / 数据报默认udp
//返回值:文件描述符-非负整数--套接字所有其它接口的操作句柄;失败返回-1
- 2.为套接字绑定地址信息
int bind(int sockfd, struct sockaddr *addr, socklen_t len);
//sockfd:创建套接字返回的操作句柄
//addr:要绑定的地址信息
//len:要绑定的地址信息长度
不同的地址结构:前两个字节是地址域类型
struct sockaddr{
sa_family_t sa_family;
char sa_data[14];
}
struct sockaddr_in{
sa_family_t sin_family; //地址域
in_port_t sin_port; //端口号
struct in_addr{
in_addr_t s_addr;
}
sin_addr; //IP地址
}
struct sockaddr_un{
_SOCKADDR_COMMON (sun_);
char sun_path[108];
}
bind可以绑定不同的地址结构,为了实现接口统一,因此用户定义地址结构的时候,定义自己需要的地址结构(例如: ipv4就使用struct sockaddr_in),但是进行绑定的时候,统一类型强转成为sockaddr 类型*
bind(fd, struct sockaddr *addr, len){
if(addr->sa_family == AF_INET){
//绑定IPV4地址信息,这个结构体按照sockaddr_in 进行解析
}
else if(addr->sa_family == AF_INET6){
//IPV6地址绑定,按照IPV6地址结构解析
}
else if(addr->sa_family == AF_LOCAL){}
}
- 3.接收数据 – 不仅仅接收数据,还要通过接收得知这个数据是谁发的,以便于进行回复
ssize_t recvfrom(int sockfd, char *buf, int len, int flag, struct sockaddr *peer_addr, socklen_t *addrlen)
//sockfd: socket操作句柄
//buf:一块缓冲区,用于接收从接收缓冲区中取出的数据
//len:想要接收的数据长度
//flag:操作选项标志,默认为0,表示阻塞操作
//peer_addr:发送的地址信息
//addrlen:想要获取的地址信息长度以及返回实际长度
//返回值:成功返回实际接收到的数据字节长度;失败返回-1
- 4.发送数据
发到发送缓冲区
ssize_t sendto(int sockfd, char *data, int len ,int flag, struct sockaddr *peer_addr, socklen_t addrlen)
//sockfd:sock操作句柄
//data:要发送的数据首地址
//len: 要发送的数据长度
//flag:默认为0,表示阻塞操作
//返回值:成功返回实际发送的数据的字节长度;失败返回-1
- 5.关闭套接字
int close(int fd);
TCP通信
tcp通信特点:面向连接,可靠传输,面向字节流
服务端:
- 1.创建套接字,在内核中创建socket结构体使进程与网卡建立联系
- 2.绑定地址信息,向内核中的socket结构体描述源端的地址信息(服务端此时已经接收到了
客户端的地址信息)
2.1 发送数据,源端地址就是绑定的地址
2.2 接收数据,告诉系统发往主机的那些数据应该放到我这里 - 3.开始监听,设置套接字的监听状态,只有处于监听状态的套接字才会接收socket结构体与
客户端进行通信(所有的客户端请求都是由一个套接字完成的) - 4.从已完成连接队列中获取一个新建连接socket,获取操作句柄,通过这个新建的指定
socket的操作句柄与相应客户端端口(刚才发送请求的客户端端口)进行通信(只要客户
端的端口号不同,都要创建一个新的socket来与之对应,所以这个socket与指定的客户端
通信) - 5.收/发数据,此时收/发数据的顺序不做要求,此时的对应的socket中包含了完整的五元组信
息,所以通信时不需要再去指定通信对端的地址信息 - 6.关闭套接字释放资源
客户端:
- 1.创建套接字
- 2.绑定地址信息(不推荐主动绑定,可以交给系统自动绑定,所以这步可以忽略)
- 3.向服务器发起连接请求
- 4.收/发数据
- 5.关闭套接字
注意:
- 当服务端没有进行到第三步(开始监听)时,客户端发送请求就会被丢失,第四步新建的socket完成并被获取之后监听状态修改为就绪状态(ESTABLSHED),在就绪状态之后才收发数据,在连接完成之前就收到数据的话,会认为状态错误,要求对方重新建立连接并且释放这个新的套接字,连接建立完成之后的所有通信都由这个新建的套接字完成
客户端对应端口发送的数据会放到服务端的对应的socket的接收缓冲区中进行处理
监听套接字,只接收第一次的连接请求,接收后新建一个套接字,这个新建的套接字与这个客户端端口号对应起来(一对一),之后的建立连接的过程和数据通信都由这个新建的套接字完成
tcp通信缺点:
- 1.服务端只能与一个客户端通信一次
- 2.因为一个执行流中进行多种可能导致阻塞的功能操作
2.1 获取新连接accept
2.2 与客户端通信recv/send
TCP通信的多执行流实现:
实现思路:在获取新连接之后,创建新的执行流负责与客户端进行通信
将不同的功能分摊到不同的执行流中,那么当其中某一个功能阻塞的话,也不会影响其它执行流的功能
多线程 灵活 资源占用少 通信方便
多进程 安全 健壮性高
若服务端针对客户端的业务比较简单则使用多线程,业务负责时推荐使用多进程
进程实现:
- 注意:父子进程数据独有,父进程应该关闭通信套接字
信号处理SIGCHLD
线程实现: - 注意:因为数据共享,通信套接字不能在主线程关闭
TCP编程接口介绍
- 1.创建套接字(在内核中创建socket结构体使进程与网卡建立联系)
int socket(int domain, int type, int protocol);
//domain:地址域-确定本次socket通信使用那种协议版本的地址结构-不同的协议版本有不同的地址结构 -- AF_INET IPV4网络协议
//type:套接字类型(流式套接字-SOCK_STREAM / 数据报套接字-SOCK_DGRAM)
//protocol:协议类型(通常就是传输层协议的选择 IPPROTO_TCP / IPPROTO_UDP),默认为0-流式默认tcp / 数据报默认udp
//返回值:文件描述符-非负整数--套接字所有其它接口的操作句柄;失败返回-1
- 2.绑定地址信息(向内核socket描述源端地址信息)
int bind(int sockfd, struct sockaddr *addr, socklen_t len);
- 3.开始监听(设置套接字的一个监听状态,只有处于监听状态的套接字才会接收客户端的连接请求)
listen(int sockfd, int backlog);
//sockfd:将那个套接字设置为监听状态,并且监听状态后可以开始接收客户端连接请求
//backlog:同一时间的并发连接数,决定同一时间最多接收多少个客户端的连接请求(防止恶意操作,不断发送连接请求)
- 4.获取新建连接(从已完成连接队列中取出一个socket,并且返回这个socket的描述符操作句柄,通过这个指定socket的操作句柄与指定客户端进行通信)
int accept(int sockfd, struct sockaddr *addr, socklen_t *len)
//sockfd:监听套接字,表示要获取那个tcp服务端套接的新建连接
//cli_addr:这个新的套接字对应的客户端地址信息 / len:地址信息长度
//返回值:是新建socket套接字的描述符 --- 外部程序中的操作句柄
- 5.接收 / 发送数据(因为tcp通信套接字中已经标识了五元组,因此不需要接收数据的时候获取对方地址信息,发送数据的时候也不需要指定对方的地址信息)(每一个新建的套接字都是服务特定的客户端)
ssize_t recv(int sockfd, char *buf, int len, int flag);
//默认阻塞,没有数据等待,
返回值:连接断开返回0,不再阻塞,出错返回值小于0,返回值大于0表示实际接收的数据长度
ssize_t send(int sockfd,char *data,int len,int flag);
//默认阻塞,缓冲区数据满了则等待,连接断开仍旧继续发送数据则触发SIGPIPE异常
- 6.关闭套接字(释放资源)
int close(fd);
- ** 7.向服务端发起连接请求**
int connect(int sockfd,struct sockaddr *srv_addr,int len);
//srv_addr:服务端地址信息
//connect这个接口会在sockfd的套接字socket中描述对端地址信息
UDP与TCP总结:
- udp通信:无连接,不可靠,面向数据报
- tcp通信:面向连接,可靠传输,面向字节流
udp就比如发短信,只需要知道目标的手机号就可以发送短信
tcp就比如打电话,不仅需要知道目标的手机号,还需要等到目标接通之后才能通话
什么是面向连接:必须建立连接,保证通信双方都具有数据收发的能力,才可以进行通信