网络中的进程通常由一个三元组来标识,包括协议、本地地址和端口。
主机字节次序与网络字节次序
对于一个4字节的整数:0x11223344
,占用四个字节存储单元,在主机中有两种主机字节次序:
Bytes3 | Bytes2 | Bytes1 | Bytes0 | |
Little Endian : | 11 | 22 | 33 | 44 |
Big Endian : | 44 | 33 | 22 | 11 |
x86采用小端方式,PowerPC和Sparc采用大端方式。
为了保证互连性,要求所有的数据按照统一字节顺序传输,于是出现了网络字节次序,规定与x86的主机字节次序相反。
在UNIX中提供如下API用于转换:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
进程经网络通信时,有两种方法可以选择:
面向连接方式,就是通信双方在通信时,要事先建立一条通信线路,
其过程有建立连接、使用连接和释放连接三个过程。
TCP协议就是一种面向连接服务的协议,电话系统是一个面向连接的模式。无连接方式,就是通信双方不需要事先建立一条通信线路,
而是把每个带有目的地址的包(报文分组)送到线路上,由系统选定路线进行传输。
IP、UDP协议就是一种无连接协议,邮政系统是一个无连接的模式。#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int socket(int domain, int type, int protocol);
Socket类型:
- 流套接字,SOCK_STREAM,用于提供面向连接、可靠的数据传输服务
- 数据包套接字,SOCK_DGRAM,用于提供无连接服务,不能保证数据的可靠性
- 原始套接字,SOCK_RAW
Raw socket与标准套接字(SOCK_STREAM、SOCK_DGRAM)的区别:
raw socket可以读写内核没有处理的IP数据包,而流套接字只能读取TCP数据,数据包套接字只能读取UDP数据。
使用raw socket可以避开TCP/IP处理机制,被传送的数据包可以被直接传送给需要它的应用程序。
raw socket植根于操作系统网络核心(Network Core),而标准socket则“悬浮”于TCP和UDP协议的外围。
套接字寻址API:
include <netdb.h>
//获取主机名和IP地址
struct hostent *gethostbyname(const char *name);
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
//获取服务与端口地址
struct servent *getservbyname(const char *name, const char *proto);
struct servent *getservbyport(int port, const char *proto);
套接字选项API:
#include <sys/types.h>
#include <sys/socket.h>
//获取套接字选项的设置情况
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
//设置套接字选项
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
套接字API:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t addrlen);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
int shutdown(int sockfd, int how);
bind函数使用通用地址结构struct sockaddr
来设置套接字地址,
而在网络通信时必须使用sockaddr_in
来构造一个套接字地址,
所以,调用bind时,需要将sockaddr_in
结构类型强制转换为struct sockaddr
结构类型。
listen函数仅被TCP服务器调用。当函数socket
创建一个套接字时,它被假设为一个主动套接字,
listen将此套接字转换为被动套接字,指示内核应接受向此套接字的连接请求,
同时维护两个队列(未完成连接队列、已完成连接队列)排队连接请求。
服务器套接字进入侦听状态后,必须通过函数accept接收客户进程提交的连接请求,从而完成一个套接字的完整连接。
套接字的数据收发即可以使用文件库函数 read/write ,也可以使用套接字的专用收发函数 recv/send 。
TCP是面向连接的通信协议,采用客户机-服务器模式。套接字的全部工作流程如下所述:
- 服务器启动进程,调用Socket创建一个基于TCP协议的流套接字描述符。
- 其次,服务进程调用bind命名套接字,将套接字描述符绑定到本地地址和本地端口上,至此Socket的半相关描述—{协议,本地地址,本地端口}—完成。
- 再次,服务器端调用listen,开始侦听客户端的Socket连接请求。
- 接下来,客户端创建套接字描述符,并且调用connect向服务端提交连接请求。服务器端接收到客户端连接请求后,调用accept接受,并创建一个新的套接字描述符与客户端建立连接,然后原套接字描述符继续侦听客户端的连接请求。
- 客户端与服务器端的新套接字进行数据传送,调用write或send向对方发送数据,调用read或recv接收数据。
- 在数据交流完毕后,双方调用close或者shutdown半闭套接字。
基于TCP(面向连接)的socket编程TCP是面向连接的,可靠的传输协议
服务器端程序:
- 创建套接字(socket)
- 将套接字绑定到一个本机地址和端口上(bind)
- 将套接字设为监听模式,准备接收客户请求(listen)
- 等待客户请求到来,当请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept)
- 用返回的套接字和客户端进行通信(send/recv)
- 返回,等待另一个用户的请求
- 关闭套接字
客户端程序:
- 创建套接字(socket)
- 向服务器端发送连接请求(connect)
- 和服务器进行通信(recv/send)
- 关闭套接字
基于UDP(面向无连接)的socket编程UDP是无连接的,不可靠的传输协议
服务器端(接收端)程序:
- 创建套接字(socket)
- 将套接字绑定到一个本地地址和端口上(bind)
- 等待接收数据(recvfrom)
- 关闭套接字
客户端(发送端)程序:
- 创建套接字(socket)
- 发送数据(sendto)
- 关闭套接字