Socket,原意插座、插口。写软件程序时,可以想象成一根网线,一头插在客户端,一头插在服务端,然后进行通信。所以通信前,双方都要建立一个Socket。
Socket编程进行的是端到端的通信,意识不到中间经过多少局域网、路由器,因而能设置参数,也只能是端到端协议之上网络层和传输层的。
在网络层,Socket函数需要指定IPv4 or IPv6,分别对应设置为:
- AF_INET
- AF_INET6
还要指定到底是TCP还是UDP:
- TCP协议是基于数据流的,所以设置为SOCK_STREAM
- UDP是基于数据报的,因而设置为SOCK_DGRAM
基于TCP协议的Socket程序函数调用过程
两端创建了Socket之后,接下来的过程中,TCP和UDP稍有不同,我们先来看TCP。
TCP的服务端要先监听一个端口,一般是先调用bind函数,给这个Socket赋予一个IP地址和端口。
为什么需要端口?
一个应用程序,当一个网络包来了,内核要通过TCP头里面的这个端口,找到你这个应用程序,把包给你。
为什么要IP地址?
有时一台机器会有多个网卡,就会有多个IP地址。可以选择监听所有网卡,也可以选择监听一个网卡,这样,只有发给这个网卡的包,才会给你。
当服务端有了IP和端口号,就能调用listen函数进行监听。服务端就进入listen状态,这时客户端即可发起连接。
在内核中,为每个Socket维护两个队列:
- 已经建立了连接的队列,这时候连接三次握手已经完毕,处于established状态
- 还没有完全建立连接的队列,这时三次握手还没完成,处于syn_rcvd状态。
接着服务端调用accept函数,拿出一个已完成的连接进行处理。若还没完成,就要等着。
在服务端等待时,客户端可通过connect函数发起连接:
- 先在参数中指明要连接的IP地址和端口号
- 然后开始发起三次握手
内核会给客户端分配一个临时端口。一旦握手成功,服务端的accept就会返回另一个Socket
监听的Socket和真正用来传数据的Socket是两个:
- 监听Socket
- 已连接Socket
连接建立成功之后,双方开始通过read和write函数来读写数据,就像往一个文件流里面写东西一样。
基于TCP协议的Socket程序函数调用过程。
TCP的Socket就是一个文件流,因为Socket在Linux中是以文件形式存在。
写入和读出都是通过文件描述符(后文简称为 fd)。
在内核中,Socket是一个文件,那对应就有fd。
每个进程都有一个数据结构task_struct,里面指向一个fd数组,列出该进程打开的所有文件的fd。
fork 之后,就会创建个该结构:
/
struct task_struct { /* these are hardcoded - don't touch */ long state; /* -1 unrunnable, 0 runnable, >0 stopped */ long counter; long priority; long signal; fn_ptr sig_restorer; fn_ptr sig_fn[32]; /* various fields */ int exit_code; unsigned long end_code,end_data,brk,start_stack; long pid,father,pgrp,session,leader; unsigned short uid,euid,suid; unsigned short gid,egid,sgid; long alarm; long utime,stime,cutime,cstime,start_time; unsigned short used_math; /* file system info */ int tty; /* -1 if no tty, so it must be signed */ unsigned short umask; struct m_inode * pwd; struct m_inode * root; unsigned long close_on_exec; struct file * filp[NR_OPEN]; /* ldt for this task 0 - zero 1 - cs 2 - ds&ss */ struct desc_struct ldt[3]; /* tss for this task */ struct tss_struct tss; };
fd是个整数,是这个数组的下标。
数组内容是个指针,指向内核中所有打开的文件的列表。是文件,就会有个inode,Socket对应的inode不像真正的文件系统保存在硬盘,而是在内存。在这个inode,指向了Socket在内核中的Socket结构。
这个结构里面,主要是两个队列:
- 发送队列
- 接收队列
这两个队列里保存的是一个缓存sk_buff。该缓存能够看到完整的包结构。
基于UDP协议的Socket程序函数调用过程
UDP没有连接,所以无需三次握手,即无需调用listen、connect。
但UDP的的交互仍需IP、端口号,因而也需要bind。
UDP没有维护连接状态,因而无需每对连接都建立一组Socket,只要有一个Socket就能和多个客户端通信。
正因为没有连接状态,每次通信时,都调用sendto、recvfrom,都可以传入IP地址和端口。
基于UDP协议的Socket程序函数调用过程