TCP/IP协议下Socket编程原理

在学习Socket编程之前,首先要了解基本的网络体系结构相关知识。参看网络模型对应关系

不同于应用层之上的Web编程,Socket编程处于应用层和网络层之间,这种API本质上是通过应用进程向OS发出系统调用实现的。

典型的网络API

  1. Unix的Socket
  2. 微软的WINSOCK
  3. AT&T的TLI Socket最初是面向TCP/IP协议栈接口,目前是事实上的工业标准,通信模型是C/S架构,为应用进程间通信提供抽象。

如果只看应用层,在同一主机上可能同时运行多个应用程序,那么如何确定通信对象呢?这个时候就需要借助传输层协议,形成IP+端口机制。

标识通信端点(对外):IP地址+端口号

OS/进程管理套接字(对内):套接字描述符

这种内部套接字管理方式类似于文件的抽象,当应用进程创建套接字时,OS分配一个数据结构并返回套接字描述符,每一个进程维护一张Socket描述符表,表项指向一个Socket数据结构

Socket数据结构

1
2
3
4
5
6
7
8
9
10
11
struct sockaddr_in {

short int sin_family; /* Address family */

unsigned short int sin_port; /* Port number */

struct in_addr sin_addr; /* Internet address */

unsigned char sin_zero[8]; /* Same size as struct sockaddr */

};

sockaddr_in 声明端点地址,TCP/IP协议族的sin_family值为AF_INET

Socket API函数

常用socket函数

  1. 创建: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
    3
    struct 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的区别

  2. 关闭:int closesocket(SOCKET sd);

    • 与文件类似,当多个进程共享一个套接字,调用后只能将引用计数减1,至0时真正关闭
    • 对于同一进程中的多个线程只计数一次,因此一个线程调用之后其他线程也无法再访问
  3. 绑定本地端点地址:int bind(sd, localladdr,addrlen)

    • localaddr对应sockaddr_in
    • 客户程序一般不调用,服务器端需要绑定熟知端口号,同时服务器端由于可能有多个IP地址,还需要一个地址通配符:INADDR_ANY,意即所匹配的任一地址有效,不指定具体IP地址。
    • 在Linux系统中,1024以下的端口只有拥有root权限的程序才能绑定。
  4. 监听:int listen(sd,queuesize);

    • 仅用于服务器端的TCP连接
    • 新的Client的连接请求先被放在接收队列中,直到Server程序调用accept函数接受连接请求
  5. 连接:connect(sd,saddr,saddrlen);

    • 仅用于客户端,但TCP和UDP均可
    • saddr即要连接的远程服务器套接字
  6. 响应连接请求:newsock = accept(sd, caddr, caddrlen);

    • 仅用于服务器端的TCP连接
    • 从监听状态的连接请求队列取出最前的一个,创建一个新的套接字来描述来与特定的Client交换信息,因为TCP是点对点协议,这样才能实现并发。
  7. 接收/发送消息:

    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是无连接的,所以需要指定发送/接收数据的对方

  8. 获得或改变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绑定的端口可以被重用。
  9. 网络字节顺序转换

    在TCP/IP结构中虽然没有表示层,但是还是要完成相应的数据格式转换功能

    • htons()–”Host to Network Short”
    • htonl()–”Host to Network Long”
    • ntohs()–”Network to Host Short”
    • ntohl()–”Network to Host Long”
  10. 服务器IP地址解析

    客户端将服务器域名或十进制IP地址转换成二进制IP地址

    • inet_addr() 十进制 → 二进制(网络字节顺序)
    • gethostbyname 域名→ 二进制,返回一个指向hostent结构的指针,*h_addr_list 表示的是主机的ip地址,注意,这个是以网络字节序存储的。
1
2
3
4
5
6
7
struct hostent {
char *h_name; /* official name of host */
char **h_aliases; /* alias list */
int h_addrtype; /* host address type */
int h_length; /* length of address */
char **h_addr_list; /* list of addresses */
}

同理,还有getservbyname返回一个servent指针实现服务名→端口号;getprotobyname返回protoent指针实现协议名→协议号

Socket TCP

socket调用基本流程

TCP客户端软件流程

  1. 确定服务器IP地址与端口号
  2. 创建套接字
  3. 分配本地端点地址(IP地址+端口号)——由系统自动完成,所以bind一般只用在服务器端
  4. 连接服务器(如果是UDP,指定服务器端点地址,构造UDP数据报)
  5. 遵循应用层协议进行通信
  6. 释放连接

循环TCP服务器流程

  1. 创建(主)套接字,绑定熟知端口号
  2. 设置(主)套接字为被动监听模式,准备用于服务器
  3. 调用accept()函数接受下一个连接请求,创建新套接字用于与该客户建立连接
  4. 遵循应用层协议,反复接受客户请求,构造并发送响应
  5. 完成为特定客户服务后,关闭与该客户之间的连接,返回步骤3

并发TCP服务器流程

主线程

  1. 创建(主)套接字,绑定熟知端口号
  2. 设置(主)套接字为被动监听模式,准备用于服务器
  3. 调用accept()函数接受下一个连接请求,创建一个新的子线程处理该客户响应

子线程

  1. 创建新的套接字接受一个客户的服务请求
  2. 遵循应用层协议,与特定客户进行交互
  3. 完成为特定客户服务后,关闭与该客户之间的连接,终止线程

在实际过程中,会有一个主线程和多个子线程同时运行,实现并发。