0%

网络编程Socket


网络中的进程通常由一个三元组来标识,包括协议、本地地址和端口

主机字节次序与网络字节次序

对于一个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是面向连接的通信协议,采用客户机-服务器模式。套接字的全部工作流程如下所述:

  1. 服务器启动进程,调用Socket创建一个基于TCP协议的流套接字描述符。
  2. 其次,服务进程调用bind命名套接字,将套接字描述符绑定到本地地址和本地端口上,至此Socket的半相关描述—{协议,本地地址,本地端口}—完成。
  3. 再次,服务器端调用listen,开始侦听客户端的Socket连接请求。
  4. 接下来,客户端创建套接字描述符,并且调用connect向服务端提交连接请求。服务器端接收到客户端连接请求后,调用accept接受,并创建一个新的套接字描述符与客户端建立连接,然后原套接字描述符继续侦听客户端的连接请求。
  5. 客户端与服务器端的新套接字进行数据传送,调用write或send向对方发送数据,调用read或recv接收数据。
  6. 在数据交流完毕后,双方调用close或者shutdown半闭套接字。

基于TCP(面向连接)的socket编程TCP是面向连接的,可靠的传输协议

服务器端程序:

  1. 创建套接字(socket)
  2. 将套接字绑定到一个本机地址和端口上(bind)
  3. 将套接字设为监听模式,准备接收客户请求(listen)
  4. 等待客户请求到来,当请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept)
  5. 用返回的套接字和客户端进行通信(send/recv)
  6. 返回,等待另一个用户的请求
  7. 关闭套接字

客户端程序:

  1. 创建套接字(socket)
  2. 向服务器端发送连接请求(connect)
  3. 和服务器进行通信(recv/send)
  4. 关闭套接字

基于UDP(面向无连接)的socket编程UDP是无连接的,不可靠的传输协议

服务器端(接收端)程序:

  1. 创建套接字(socket)
  2. 将套接字绑定到一个本地地址和端口上(bind)
  3. 等待接收数据(recvfrom)
  4. 关闭套接字

客户端(发送端)程序:

  1. 创建套接字(socket)
  2. 发送数据(sendto)
  3. 关闭套接字

  1. socket与TCP/UDP编程
  2. 基于Socket的UDP和TCP编程介绍