在学习Socket编程之前,首先要了解基本的网络体系结构相关知识。参看网络模型对应关系
不同于应用层之上的Web编程,Socket编程处于应用层和网络层之间,这种API本质上是通过应用进程向OS发出系统调用实现的。
典型的网络API
如果只看应用层,在同一主机上可能同时运行多个应用程序,那么如何确定通信对象呢?这个时候就需要借助传输层协议,形成IP+端口机制。
标识通信端点(对外):IP地址+端口号
OS/进程管理套接字(对内):套接字描述符
这种内部套接字管理方式类似于文件的抽象,当应用进程创建套接字时,OS分配一个数据结构并返回套接字描述符,每一个进程维护一张Socket描述符表,表项指向一个Socket数据结构 。
Socket数据结构
1 | struct sockaddr_in { |
sockaddr_in
声明端点地址,TCP/IP协议族的sin_family
值为AF_INET
Socket API函数
创建:
socket(protofamily, type, proto)
返回套接字描述符
protofamily = PF_INET
type = SOCK_STREAM/SOCK_DGRAM/SOCK_RAW
其中 流式套接字SOCK_STREAM (TCP)、数据报套接字SOCK_DGRAM (UDP) 工作在传输层,原始套接字SOCK_RAW 工作在网络层。SOCK_RAW 可以处理ICMP、IGMP等网络报文、特殊的IPv4报文、可以通过IP_HDRINCL套接字选项由用户构造IP头,创建时需要特殊权限。
protocol参数一般取0
例如
1
2
3struct protoent *p;
p = getprotobyname("tcp");
SOCKET sd = socket(PF_INET,SOCK_STREAM,p -> p_proto);关于TCP/IP参数为什么两者不同,理论上建立socket时是指定协议,应该用PF_xxxx,设置地址时应该用AF_xxxx。当然AF_INET和PF_INET的值是相同的,混用也不会有太大的问题。关于PF_INET和AF_INET的区别
关闭:
int closesocket(SOCKET sd);
- 与文件类似,当多个进程共享一个套接字,调用后只能将引用计数减1,至0时真正关闭
- 对于同一进程中的多个线程只计数一次,因此一个线程调用之后其他线程也无法再访问
绑定本地端点地址:
int bind(sd, localladdr,addrlen)
- localaddr对应sockaddr_in
- 客户程序一般不调用,服务器端需要绑定熟知端口号,同时服务器端由于可能有多个IP地址,还需要一个地址通配符:
INADDR_ANY
,意即所匹配的任一地址有效,不指定具体IP地址。 - 在Linux系统中,1024以下的端口只有拥有root权限的程序才能绑定。
监听:
int listen(sd,queuesize);
- 仅用于服务器端的TCP连接
- 新的Client的连接请求先被放在接收队列中,直到Server程序调用accept函数接受连接请求
连接:
connect(sd,saddr,saddrlen);
- 仅用于客户端,但TCP和UDP均可
- saddr即要连接的远程服务器套接字
响应连接请求:
newsock = accept(sd, caddr, caddrlen);
- 仅用于服务器端的TCP连接
- 从监听状态的连接请求队列取出最前的一个,创建一个新的套接字来描述来与特定的Client交换信息,因为TCP是点对点协议,这样才能实现并发。
接收/发送消息:
TCP方式:
int send(sd, *buf, len, flags);
int recv(sd, *buf, len, flags);
UDP方式:
int sendto(sd, *buf, len, flags, destaddr, addlen);
int recvfrom(sd,*buf, len, flags,senderadde,saddrlen);
由于UDP是无连接的,所以需要指定发送/接收数据的对方
获得或改变socket属性:
int getsockopt(int sd, int level, int optname, char *optval, int *optlen);
int setsockopt(int sd, int level, int optname, char *optval, int *optlen);
常用属性:
- SO_RCVTIMEO,SO_SNDTIMEO:获得或设置socket发送/接收的timeout
- SO_SNDBUF,SO_RCVBUF:获得或设置socket发送/接收的buffer大小
- SO_BROADCAST:获得或设置socket状况,使之可以广播发送数据报(只能用于UDP方式)
- SO_REUSEADDR:设置该socket绑定的端口可以被重用。
网络字节顺序转换
在TCP/IP结构中虽然没有表示层,但是还是要完成相应的数据格式转换功能
- htons()–”Host to Network Short”
- htonl()–”Host to Network Long”
- ntohs()–”Network to Host Short”
- ntohl()–”Network to Host Long”
服务器IP地址解析
客户端将服务器域名或十进制IP地址转换成二进制IP地址
inet_addr()
十进制 → 二进制(网络字节顺序)gethostbyname
域名→ 二进制,返回一个指向hostent结构的指针,*h_addr_list
表示的是主机的ip地址,注意,这个是以网络字节序存储的。
1 | struct hostent { |
同理,还有getservbyname
返回一个servent
指针实现服务名→端口号;getprotobyname
返回protoent
指针实现协议名→协议号
Socket TCP
TCP客户端软件流程
- 确定服务器IP地址与端口号
- 创建套接字
- 分配本地端点地址(IP地址+端口号)——由系统自动完成,所以bind一般只用在服务器端
- 连接服务器(如果是UDP,指定服务器端点地址,构造UDP数据报)
- 遵循应用层协议进行通信
- 释放连接
循环TCP服务器流程
- 创建(主)套接字,绑定熟知端口号
- 设置(主)套接字为被动监听模式,准备用于服务器
- 调用accept()函数接受下一个连接请求,创建新套接字用于与该客户建立连接
- 遵循应用层协议,反复接受客户请求,构造并发送响应
- 完成为特定客户服务后,关闭与该客户之间的连接,返回步骤3
并发TCP服务器流程
主线程:
- 创建(主)套接字,绑定熟知端口号
- 设置(主)套接字为被动监听模式,准备用于服务器
- 调用accept()函数接受下一个连接请求,创建一个新的子线程处理该客户响应
子线程:
- 创建新的套接字接受一个客户的服务请求
- 遵循应用层协议,与特定客户进行交互
- 完成为特定客户服务后,关闭与该客户之间的连接,终止线程
在实际过程中,会有一个主线程和多个子线程同时运行,实现并发。