计算机网络:Socket编程


Socket 简介

Socket,即套接字,可用于网络中两个不同主机上的进程间通信
最早由 BSD 实现,后来Unix、Linux、Win等均有实现及拓展

先看一下 Socket 编程在计算机网络中所处的位置
网络编程
实际上,SOCK_RAW 类型的 socket 是直接面向网络层的

Socket API 主要面向 TCP/IP协议栈,为 Client/Sever 通信模型提供支持
对外使用 IP地址与端口号(port)标识通信端点,即用 IP + port 作为通信端点地址
进程使用套接字描述符(socket descriptor,类似文件描述符) 对socket 进行管理
与文件管理类似,套接字描述符表的每个表项都指向一个保存套接字信息的数据结构
套接字描述符即是表中的下标


Socket 编程的基本操作

各平台的 Socket 实现核心部分基本一致,下面简单介绍

socket

创建套接字

int socket(int domain, int type, int protocol);

参数

  • 指明协议族,如 PF_INET 表示 TCP/IP
  • 指明创建的 socket 类型,SOCK_STREAM、SOCK_DGRAM 和 SOCK_RAW 等
  • 指明具体协议,可配合 getprotobyname使用

返回值:创建成功时返回 socket descriptor,否则返回 -1

int sd = socket(PF_INET, SOCK_DGRAM, getprotobyname("tcp")->p_proto);

其中,SOCK_STREAM、SOCK_DGRAM 和 SOCK_RAW 类型分别对应于
TCP、UDP 和网络层协议(如 IP、ICMP、IGMP

TCP:可靠、有连接、字节流传输、点对点
(一个 TCP socket 只能与一个 TCP socket 通信、也只能建立一次连接
UDP:不可靠、无连接、数据报传输
(UDP socket 可多次指定目标地址


close / closesocket

关闭指定描述符对应的 socket,Win 下为 closesocket

int close(int sd);

参数:要关闭的 socket 的描述符
返回值:成功则返回 0,失败返回 -1
注意:多个进程共享的 socket 会有引用计数,一个进程中 close() 引用计数减 1
减至 0 时 socket 关闭;而同一进程的多个线程间共享的 socket 不进行引用计数


bind

为套接字绑定本地端点地址(IP 与 port

int bind(int sd, const struct sockaddr *localaddr, int addrlen);

参数

  • 套接字描述符
  • 端点地址,根据 socket 类型可能有多种结构体,因此指定 addrlen,表明该结构体长度
  • 参数 2 的实际类型长度

返回值:成功则返回 0,否则返回 -1

注意:服务端 socket 需要 bind(),客户端 socket 一般无需使用 bind() 而由操作系统完成

// Declare variables
SOCKET ListenSocket;
struct sockaddr_in saServer;
hostent* localHost;
char* localIP;

// Create a listening socket
ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

// Get the local host information
localHost = gethostbyname("");
localIP = inet_ntoa (*(struct in_addr *)*localHost->h_addr_list);

// Set up the sockaddr structure
saServer.sin_family = AF_INET;
saServer.sin_addr.s_addr = inet_addr(localIP);
saServer.sin_port = htons(5150);

// Bind the listening socket using the
// information in the sockaddr structure
bind( ListenSocket,(SOCKADDR*) &saServer, sizeof(saServer) );

//以上来自于 microsoft docs

若服务器有两张网卡,分别连接了不同的网络,作为服务端如果给 socket
设定其中一个网卡的 IP,那么就无法接受发送到另一个 IP 的请求

面对这样的情况,可以将地址设为通配符 INADDR_ANY,即
saServer.sin_addr.s_addr = INADDR_ANY;


listen

将服务端套接字置于监听状态(仅服务端流式套接字(TCP套接字)使用

int listen(int sd, int queuesize);

参数

  • 调用 listen() 的 socket descriptor
  • 等待队列的最大大小

返回值:成功则返回 0,否则返回 -1


connect

使客户端 socket 与特定计算机特定端口的 socket 进行连接

int connect(int sd, const sockaddr *addr, int addrlen);

参数:

  • 发起连接的客户端 socket
  • 要连接的服务端端点地址
  • 参数 2 的实际类型长度

返回值:成功则返回 0,否则返回 -1

connect() 用于 TCP 客户端 socket 时,将向服务端点 addr 发送建立 TCP 连接请求,此时 connect() 是阻塞的
         用于 UDP 客户端 socket 时,仅仅为客户端 socket 指定了服务端点地址 addr
也就是说,TCP socket 调用 connect() 成功后即建立连接可以通信,UDP socket 只是指定了通信端点

注意:TCP socket 只能调用一次 connect(),UDP socket 可以多次调用 connect()


accept

从监听状态的服务端 socket 接收客户端连接请求,然后创建一个新的 socket 连接客户端 socket
(仅服务端流式套接字(TCP套接字)使用

int accept(int sd, struct sockaddr *addr, int *addrlen);

参数:

  • 监听状态(listen)的服务端 socket
  • 新创建套接字的端点地址结构体
  • 参数 2 的实际长度

返回值:返回响应客户端 socket 请求所创建的 socket 描述符,以用于通信
当请求队列为空时,调用 accept() 的线程阻塞


send、sendto

向一个 socket 所指定的端点地址发送数据

//win
int send(SOCKET sd, const char *buf, int len, int flags);

//Linux
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数:

  • 发送数据所使用的 socket
  • 数据起始地址
  • 数据长度
  • 一组标志,用于指定调用的方式,通常为 0

返回值:函数返回实际发送的字节数,可能小于 len,出错则返回 -1

注意,调用 send() 的套接字必须已有通信端点地址,如建立连接的 TCP socket、调用过 connect() 的 UDP socket

若 socket 未指定过通信端点,可使用 sendto() 在通信时指定

//win
int sendto(SOCKET sd, const char *buf, int len, int flags, const struct sockaddr *to, int tolen);

//Linux
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

同理,使用 sendto() 的 socket 只能是 UDP socket
对于已经使用 connect() 绑定过端点地址的 socket,sendto() 仅改变本次发送的目标地址

send(sd, buf, len, flag)相当于 sendto(sd, buf, len, flag, NULL, 0)

此外,Socket API 还提供了 sendmsg() / WSASendMsg() 等写入函数


recv、recvfrom

recv() 用于 TCP socket 从连接的另一端接收数据,或用于调用过 connect() 的 UDP 客户端 socket 接收数据

recvfrom() 用于 UDP 服务器 socket 或未调用过 connect() 的 UDP socket 接收数据

//win
int recv(char *buf, int len, int flags);
int recvfrom(SOCKET sd, char *buf, int len, int flags, struct sockaddr *from, int *fromlen);

//Linux
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);

返回值:返回接收的字节数,或 -1 表示出错

此外,Socket API 还提供了 recvmsg() 等函数接收数据


setsockopt、getsockopt

设置 / 获取套接字选项对应的取值

int setsockopt(SOCKET s, int level, int optname, const char *optval,  int optlen);

int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);

参数

  • 标识套接字的描述符
  • 定义选项的级别(如 SOL_SOCKET)
  • 要为其设置值的套接字选项(如 SO_BROADCAST)。optname参数必须是在指定级别内定义的套接字选项,否则是未定义行为
  • 指向存放选项值的缓冲区的指针
  • optval 参数指向的缓冲区的大小

返回值:成功时返回 0,失败返回 -1


注意,以下以 WSA 开头的 API 均为 Win 下特有的或需要的 API
Win 下需包含 WinSock2.h 头文件

WSAStartup

Win 下使用 Socket 前必须调用该函数,用于引入相应的 DLL

int WSAStartup( WORD wVersionRequested, LPWSADATA lpWSAData);

参数

  • 指明所使用的 WinSock 版本,高位字节表示副版本号,低位字节表示主版本号
  • 为具体版本信息结构体的指针,指定的 WinSock 具体版本信息将保存到传入的结构体中

返回值:成功则返回 0,失败返回非零值

WSADATA wsaData;
wVersionRequested = MAKEWORD(2,1);//version 2.1
err = WSAStartup(wVersionRequested, &wsaData);

WSACleanup

不再使用 Socket 后,调用该函数解除与 DLL 的关联,释放相应资源

int WSACleanup();

返回值:成功则返回 0


字节序

此外,网络中的各个机器可能采用不同的字节序,那么通信时应当统一一种机器无关的字节序
TCP/IP 定义了标准的用于协议头中的二进制数字节序
使用如下函数在本地字节序与网络字节序中进行转换

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

如 htons,即 host to network short


socket 通信的一般流程
socket 通信的一般流程
close 一个 TCP socket 时,也会给对方发送连接关闭的信息,图中未表示出


2020/5/1

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值