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. 完成为特定客户服务后,关闭与该客户之间的连接,终止线程

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