提示:本篇文章仅作为自己学习笔记,方便自己查询
socket编程中的几个概念
TCP协议
TCP(Transmission Control Protocol 传输控制协议)是一种面向连接、可靠的、基于字节流的传输层通信协议,数据可以准确发送,数据丢失会重发。TCP协议常用于web应用中。
TCP连接(三次握手)
TCP传输起始时,客户端、服务端要完成三次数据交互工作才能建立连接,常称为三次握手。可以比喻为以下对话:
客户端:服务端您好,我有数据要发给你,请求您开通访问权限。
服务端:客户端您好,已给您开通权限,您可以发送数据了。
客户端:收到,谢谢。
具体示意图为: 这里的SYN和ACK都是标志位,其中SYN代表新建一个连接,ACK代表确认。其中m、n都是随机数。具体说明如:
- 第一次握手:SYN标志位被置位,客户端向服务端发送一个随机数m。
- 第二次握手:ACK、SYN标志位被置位。服务端向客户端发送m+1表示确认刚才收到的数据,同时向客户端发送一个随机数n。
- 第三次握手:ACK标志位被置位,客户端向服务端发送n+1表示确认收到数据。
TCP断开(四次挥手) TCP断开连接时,客户端、服务端要完成四次数据交互工作才能建立连接,常称为四次挥手。可比喻为:
客户端:服务端您好,我发送数据完毕了,即将和您断开连接。 服务端:客户端您好,我稍稍准备一下,再给您断开 服务端:客户端您好,我准备好了,您可以断开连接了。 客户端:好的,合作愉快!
具体示意图为: 为什么建立连接只需要三次数据交互,而断开连接需要四次呢?
建立连接时,服务端在监听状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送
UDP协议
UDP(User Datagram Protocol, 用户数据报协议)是一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务,可以保证通讯效率,传输延时小。例如视频聊天应用中用的就是UDP协议,这样可以保证及时丢失少量数据,视频的显示也不受很大影响。
套接字概述
套接字就是网络编程的ID。网络通信,归根到底还是进程间的通信(不同计算机上的进程间通信)。在网络中,每一个节点(计算机或者路由器)都有一个网络地址,两个进程通信时,首先确定各自所在网络节点的网络地址。但是,网络地址只能确定进程所在的计算机,而一台计算机上可能同时运行着多个进程,所以仅凭网络地址还不能确定到底是和网络中哪个进程通信,因此套接口中还需要有其他的信息,也就是端口号(port)。在一台计算机中,一个端口号一次只能分配给一个进程,也就是说,在一台计算机中,端口号和进程间是一一对应的关系。所以,使用端口号和网络地址的组合就能唯一确定整个网络中的一个网络进程。
把网络地址和端口号信息放在一个结构体中,也就是套接口地址结构,大多数的套接口函数都需要一个指向套接口地址的指针作为参数,并以此来传递地址信息。每个协议族都定义了他自己的套接口地址结构,套接字地址结构都以 “sockaddr_” 开头,并以每个协议族名中两个字母作为结尾。
下面是socket所在的位置: 可以看到套接口有三种类型:
1)流式套接字(SOCK_STREAM) 流式套接字提供可靠的、面向连接的通信流,保证数据传输的可靠性和按序收发。TCP通信使用的就是流式套接字。 2)数据包套接字(SOCK_DGRAM) 数据报套接字实现了一种不可靠、无连接的服务。数据通过相互独立的报文进行传输,是无序的,并且不保证可靠的传输。UDP通信使用的就是数据报套接字。 3)原始套接字(SOCK_RAM) 原始套接字允许对底层协议(如IP或ICMP)进行直接访问,他功能强大但使用较为不方便,主要用于一些协议的开发。
IP地址转换
IP地址有两种不同格式:十进制点分形式和32位二进制形式。前者是用户熟悉的形式,而后者则是网络传输中IP地址的存储方式。
这里主要介绍IPV4地址转换函数,主要有 inet_addr() 、inet_aton() 、inet_ntoa() 。前两者的功能都是将字符串转换成32位网络字节序二进制值,第三个将32位网络字节序二进制地址转换成点分十进制的字符串。
字节序
字节序又称为主机字节序 Host Byte Order,HBO,是指计算机中多字节整型数据的存储方式。字节序有两种:大端(高位字节存储在低位地址,低位字节存储在高位地址)和小端(和大端序相反,PC通常采用小端模式)。
为什么需要字节序?在网络通信中,发送方和接收方有可能使用不同的字节序; 为了保证数据接受后能被正确的解析处理,统一规定:数据以高位字节优先顺序在网络上传输。因此数据在发送前和接收后都需要在主机字节序和网络字节序之间转换。 1)函数说明 字节序转换涉及4个函数:htons() 、ntohs() 、htonl() 和 ntohl() 。这里的 h 代表 host , n 代表 network , s 代表 short , l 代表 long 。通常 16bit 的IP端口号用前两个函数处理,而 IP 地址用后两个函数来转换。调用这些函数只是使其得到相应的字节序,用户不需要知道该系统的主机字节序和网络字节序是否真的相等。如果两个相同不需要转换的话,该系统的这些函数会定义成空宏。
2)函数格式
Linux下的socket API接口
1)创建socket:socket( )函数
函数原型:
int socket(int af,int type,int protocol)
- af参数:af为协议族,地址族(Address Family),也就是IP地址类型,常用的有AF_INET:IPv4协议
AF_INET6:IPv6协议 AF_LOCAL:UNIX域协议 AF_ROUTE:路由套接字 AF_KEY:密钥套接字 - type参数:types指明通信字节流类型,其取值如:
SOCK_STREAM:流式套接字(TCP方式) SOCK_DGRAM:数据包套接字(UDP方式) SOCK_RAM:原始套接字 - protocol参数:protocol 表示传输协议,常用的有 IPPROTO_TCP 和IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。通常设置为0。
使用示例:
创建套接字:
int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
创建UDP套接字:
int udp_socket = socket(AF_INET, SCOK_DGRAM, IPPROTO_UDP);
2)绑定套接字:bind()函数
函数原型:
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);
- sock参数:sock为 socket 描述符。
- addr :绑定的地址
- addrlen:地址长度
这里my_addr是IPv4地址,IPv4 套接口地址数据结构以socketaddr_in 命名,定义在 <netinet/in.h>头文件中,形式如下:
struct sockaddr_in
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
;
sin_famliy 为套接字结构协议族,如IPv4为AF_INET;
sin_port 是16位 TCP或UDP端口号,网络字节顺序;
结构体成员in_addr也是一个结构体,定义如下:
struct in_addr
{
uint32_t s_addr;
};
这里s_addr为32位IPv地址,网络字节顺序;本地地址可以用INADDR_ANY;
使用示例: 将创建的套接字sockfd与本地IP、端口2345进行绑定:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 2345
int main()
{
int sockfd;
struct sockaddr_in addr;
int addr_len = sizeof(struct sockaddr_in);
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
perror("socket fail");
exit(-1);
}
else
{
printf("socket created successfully!\nsocket id is %d\n",sockfd);
}
memset(&addr,0,addr_len);
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sockfd,(struct sockaddr *)(&addr),addr_len) < 0)
{
perror("bind error");
exit(-1);
}
else
{
printf("bind port successfully!\nlocal port:%d\n",PORT);
}
return 0;
}
3)建立连接:connect()函数
函数原型:
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);
参数与bind()参数类似
4)监听:listen()函数
函数原型:
int listen(int sock,int backlog);
- sock参数:sock为需要进入监听状态的套接字。
- backlog参数:backlog 为请求队列的最大长度。
需要注意的是listen 并未真正的接受连接,只是设置socket 的状态为监听模式,真正接受客户端连接的是accept 函数。通常情况下,listen 函数会在 socket ,bind 函数之后调用,然后才会调用 accept 函数。
listen函数只适用于SOCK_STREAM或SOCK_SEQPACKET 的socket 类型。如果socket 为 AF_INET ,则参数 backlog 最大值可设至128,即最多可以同时接受128个客户端的请求。
5)接受请求:accept()函数
函数原型:
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen)
- sock参数:sock为服务器端套接字
- addr参数:addr为sockaddr_in结构体变量。
- addrlen参数:addrlen 为参数 addr 的长度,可由 sizeof() 求得。
- 返回值:一个新的套接字,用于与客户端通信。
使用示例:
int ClientSock = accept(ServerSock, (SOCKADDR*)&ClientAddr, &len);
6)关闭:close()函数
int close(int fd);
-fd :要关闭的文件描述符
7)数据的接受和发送
数据的收发函数有几组:
- read()/write()
- recv()/send()
- readv()/write()
- recvmsg()/sendmsg()
- recvfrom()/sendto()
函数原型:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
recv()函数:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- sockfd参数:sockfd为要接收数据的套接字。
- buf参数:buf 为要接收的数据的缓冲区地址。
- len参数:len 为要接收的数据的字节数。
- flags参数:flags 为接收数据时的选项,常设为0。
send()函数:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- sockfd参数:sockfd为要发送数据的套接字。
- buf参数:buf 为要发送的数据的缓冲区地址。
- len参数:len 为要发送的数据的字节数。
- flags参数:flags 为发送数据时的选项,常设为0。
TCP、UDP通信的socket编程过程图
参考文章:
【socket笔记】TCP、UDP通信总结 Linux操作系统-高级编程-socket编程
|