HTTP/1.1 200 OK
基本原理
IP地址和端口号
IP地址用于区分不同的主机,而端口号则用于区分某个主机上不同的应用程序,端口号为0-65535,一般1024以下保留做系统使用。当我们进行socket编程时,必须区分端口号从而找到正确的应用程序。
socket通信过程
TCP(流式套接字SOCK_STREAM)
服务器端编程步骤
- 创建套接字(socket())
- 绑定套接字到一个IP地址和一个端口上(bind());
- 将套接字设置为监听模式等待连接请求(listen());
- 请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());
- 用返回的套接字和客户端进行通信(send()/recv());
- 返回,等待另一连接请求;
- 关闭套接字(closesocket())。
客户端编程步骤
- 加载套接字库,创建套接字(WSAStartup()/socket());
- 向服务器发出连接请求(connect());
- 和服务器端进行通信(send()/recv());
- 关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup())。
UDP(报文式套接字SOCK_DGRAM)
服务器端编程步骤
- socket: 建立一个socket
- bind: 将这个socket绑定在某个端口上(AF_INET)
- recvfrom: 如果没有客户端发起请求,则会阻塞在这个函数里
- close: 通信完成后关闭socket
客户端编程步骤
- socket: 建立一个socket
- sendto: 向服务器的某个端口发起请求(AF_INET)
- close: 通信完成后关闭socket
原始套接字(SOCK_RAW)
常用API
socket()函数
声明
1 | int socket(int domain, int type, int protocol); |
类似于文件的打开操作,socket创建一个socket描述符,通过该描述符进行一些读写操作。
参数
- domain
指定协议族(family),决定了socket的地址类型。
名称 | 含义 | 名称 | 含义 |
---|---|---|---|
PF_UNIX,PF_LOCAL | 本地通信 | PF_X25 | ITU-T X25 / ISO-8208协议 |
AF_INET,PF_INET | IPv4 Internet协议 | PF_AX25 | Amateur radio AX.25 |
PF_INET6 | IPv6 Internet协议 | PF_ATMPVC | 原始ATM PVC访问 |
PF_IPX | IPX-Novell协议 | PF_APPLETALK | Appletalk |
PF_NETLINK | 内核用户界面设备 | PF_PACKET | 底层包访问 |
- type
指定socket类型
名称 | 含义 |
---|---|
SOCK_STREAM | Tcp连接,提供序列化的、可靠的、双向连接的字节流。支持带外数据传输 |
SOCK_DGRAM | 支持UDP连接(无连接状态的消息) |
SOCK_SEQPACKET | 序列化包,提供一个序列化的、可靠的、双向的基本连接的数据传输通道,数据长度定常。每次调用读系统调用时数据需要将全部数据读出 |
SOCK_RAW | RAW类型,提供原始网络协议访问 |
SOCK_RDM | 提供可靠的数据报文,不过可能数据会有乱序 |
- protocol
指定协议,常用的协议有IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
bind()函数
bind()函数将一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
声明
1 | int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
参数
sockfd,即要操作的socket描述字,socket()函数的返回值。
addr,一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同。
1 | // ipv4 |
addrlen,对应地址的长度。
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。所以通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
listen()与connect()函数
声明
1 | // listen 服务器端,用于监听套接字 |
参数
- listen
第一个参数sockfd即为要监听的socket描述字,第二个参数blacklog为相应socket可以排队的最大连接个数。
socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
- connect
第一个参数sockfd即为客户端的socket描述字,第二个参数addr为要连接的服务器的socket地址,第三个参数addrlen为服务器socket地址的长度。客户端通过调用connect()函数来建立与TCP服务器的连接。
accept()
在服务端开始监听套接字、客户端发起connect请求后,TCP服务器监听到该请求后,会调用accept()函数接收请求,之后可以开始网络IO操作,类似于普通文件读写。
声明
1 | int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); |
注意事项
accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
recvfrom()
UDP中接收客户端发起的请求,如果没有,那么会阻塞在这个函数中
sendto()
UDP中发送数据
实例
TCP套接字编程
server端
1 |
|
client端
1 |
|
UDP套接字编程
server端
1 |
|
client端
1 |
|
注意事项
区分监听套接字与已连接套接字
在服务器端中,我们实际上用到了两个socket,它们分别为:
- 由listen函数将socket函数创建的socket转换而成的监听套接字,记作listenfd
- 由accept函数等待来自客户端的连接请求到达listenfd,返回一个已连接套接字,记作connfd
两者区别如下:
监听套接字,是服务器作为客户端连接请求的一个端点,它被创建一次,并存在于服务器的整个生命周期。
已连接套接字是客户端与服务器之间已经建立起来了的连接的一个端点,服务器每次接受连接请求时都会创建一次已连接套接字,它只存在于服务器为一个客户端服务的过程中。