本篇笔记基于尹圣雨的《TCP/IP编程》,主要记录Socket编程中比较关键的一些知识点和C函数。本笔记会像书中一样同时介绍Windows和Linux两个操作系统下的Socket函数,特别是两个操作系统下操作不同的地方。
套接字通信基本流程
套接字概念
套接字(socket)实际上就是网络数据传输用的软件设备,与其他用户通信的数据都需要通过这个软件接口进行传输。在代码中,它以整型变量的形式存在,实际上是由操作系统管理的。Linux操作系统把套接字和文件描述符看做是一个东西,所以对文件操作的函数大部分也可以对套接字使用,而windows认为套接字独立于文件描述符。
服务端方面
只考虑最简单的一对一通信情况,服务端需要建立好套接字,将套接字与本机的一个端口绑定,然后监听套接字是否被请求连接,如果收到连接请求,就接受连接请求并读取数据。
客户端方面
客户端与服务端对比较为简单,只需要向某主机的一个端口发起连接请求,待被接受后发送数据。
Linux下的Socket基本函数
1 2 3 4 5 6 7 8 9 10 11 #include <sys/socket.h> int socket (int domain, int type, int protocol) ;int bind (int sockfd, struct sockaddr *myaddr, socklen_t addrlen) ;int listen (int sockfd, int backlog) ;int accept (int sockfd, struct sockaddr *addr, socklen_t *addrlen) ;int connet (int sockfd, struct sockaddr *serv_addr, socklen_t addrlen) ;
由于Linux下对socket的操作与对文件无异,所以这里再介绍一下Linux下的文件操作函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open (const char *path,int flag) ;#include <unistd.h> int close (int fd) ;ssize_t write (int fd, const void * buf, size_t nbytes) ;ssize_t read (int fd, void * buf, size_t nbytes) ;
Windows下的socket基本函数
环境准备
为了在Windows下开发网络程序,需要包含头文件winsock2.h
并添加依赖项,即链接ws2_32.lib
库。
函数介绍
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <winsock2.h> int WSAStartup (WORD wVersionRequested, LPWSADATA lpwSAData) ;int WSACleanup (void ) ;SOCKET socket (int af, int type, int protocol) ;int bind (SOCKET s, const struct sockaddr * name, int namelen) ;int listen (SOCKET s, int backlog) ;SOCKET accept (SOCKET s, sturct sockaddr * addr, int * addrlen) ;int connect (SOCKET s, const struct sockaddr * name, int namelen) ;int closesocket (SOCKET s) ;
WSAStartup
该函数第一个参数是需要的Winsock版本信息,通常用MAKEWORD 宏函数构造一个版本号。第二个参数填入WSADATA
类型变量的地址,调用完该函数后,WSADATA
会填充初始化的库信息。
其余函数的使用方法都与Linux下大同小异,只是宏变量有所区别。
数据传输
Windows中的套接字返回的不是文件描述符而是句柄(handle) ,所以并不能直接用操作文件的函数来操作套接字。故引入以下两个新的函数:
1 2 3 4 5 6 int send (SOCKET s, const char * buf, int len, int flags) ;int recv (SOCKET s, const char * buf, int len, int flags) ;
套接字类型与协议设置
本小节介绍在创建套接字时使用的三个参数,在两个平台下的操作是相同的。
协议类型
创建socket的函数中第一个要求我们指定使用的协议族,协议族包含以下几种:
名称 协议族 P F _ I N E T I P v 4 互联网协议族 P F _ I N E T 6 I P v 6 互联网协议族 P F _ L O C A L 本地通信的 U N I X 协议族 P F _ P A C K E T 底层套接字的协议族 P F _ I P X I P X N o v e l l 协议族 \begin{array}{|c|c|}
\hline
名称 & 协议族 \\
\hline
PF\_INET & IPv4互联网协议族 \\
\hline
PF\_INET6 & IPv6互联网协议族 \\
\hline
PF\_LOCAL & 本地通信的UNIX协议族 \\
\hline
PF\_PACKET & 底层套接字的协议族 \\
\hline
PF\_IPX & IPX Novell协议族 \\
\hline
\end{array}
名 称 P F _ I N E T P F _ I N E T 6 P F _ L O C A L P F _ P A C K E T P F _ I P X 协 议 族 I P v 4 互 联 网 协 议 族 I P v 6 互 联 网 协 议 族 本 地 通 信 的 U N I X 协 议 族 底 层 套 接 字 的 协 议 族 I P X N o v e l l 协 议 族
由于IPv6协议族还未普及,所以大部分情况下我们使用IPv4协议族。
套接字类型
面向连接的套接字(SOCK_STREAM)
特点是没有数据边界、按序传输和保证交付 。由于没有数据边界这个特点,在使用该类型套接字时通常需要自己设计协议。
面向消息的套接字(SOCK_DGRAM)
该类型的套接字有以下特点:不保证交付、限制数据大小、有数据边界 ,虽然听起来不怎么靠谱,但是这种套接字有个非常大的优势,就是速度快。
最终选择的协议
第三个参数令人疑惑,很多人以为“套接字类型”这个参数就指定了使用的协议,但实际上并不是。只是在IPv4协议族下只有TCP这一个面向连接的套接字,有可能遇到“同一个协议族中存在多个数据传输方式相同的协议”。
对于TCP协议,该参数填写IPPROTO_TCP
;对于UDP协议,该参数填写IPPROTO_UDP
。
地址族与数据序列
绑定地址信息
在使用socket通信时,通常需要同时绑定地址和端口号,这里仅介绍绑定IPv4的地址的方法。需要先声明一个sockaddr_in
类型的结构体,其原型如下:
1 2 3 4 5 6 7 struct sockaddr_in { sa_family_t sin_family; uint16_t sin_port; struct in_addr sin_addr ; char sin_zero[8 ]; }
sockaddr
是通用的地址,而sockaddr_in
是专为IPv4协议设计的,故大小存在差异。
字节序转换
由于数据在不同CPU体系架构下的保存方式不同,所以需要统一转换微网络序(即大端序)。可以使用以下四个函数:
1 2 3 4 unsigned short htons (unsigned short ) ; unsigned short ntohs (unsigned short ) ; unsigned long htonl (unsigned long ) ; unsigned long ntohl (unsigned long ) ;
虽然有些主机的数据储存方式本身为大端序,但是为了程序的可移植性,还是需要在程序中加入此语句。
初始化地址
将点分十进制法 表示的地址转换为整型是十分棘手的,幸运的是socket库中自带了相关的转换函数;
1 2 3 4 5 6 #include <arpr/inet.h> in_addr_t inet_addr (const char * string ) ; int inet_aton (const char * string , struct in_addr *addr) ; char * inet_ntoa (struct in_addr adr) ;
以上便是socket的基本函数以及创建socket套接字的主要流程。