分类: C/C++
2014-07-20 21:46:56
原文地址:Socket 编程实现 作者:erikyo123
一、 定义
在同一个设备上运行的进程之间的通信有很多种方式,包括管道、FIFO、消息队列、信号量以及共享内存。然而如果要实现不在同一台设备上运行的进程之间的通信就需要利用socket接口。
Socket通信既可以用于不在同一台设备上运行的几个进程之间的通信,也可以用于运行在同一台设备上的几个进程之间的通信。
本文主要介绍在因特网上通信的socket编程。
二、 基本概念
在socket通信的学习过程中,我们需要先解释几个概念,以方便后续的学习。
域(domain):
他确定通信的特性,包括地址格式。各个域都有自己的格式表示地址,而表示各个域的常数都以AF_开头,意指地址族(address family)。通常有以下几类socket通信域:
AF_INET IPv4因特网域
AF_INET6 IPv6因特网域
AF_UNIX UNIX域
AF_UNSOEC 未指定
套接字类型:
套接字的类型进一步确定了通信的特征,主要有以下类型可以选择:
SOCK_DGRAM 长度固定的、无连接的不可靠报文传递
SOCK_RAW IP协议的数据包接口
SOCK_SEQPACKET 长度固定、有序、可靠的面向连接报文传递
SOCK_STREAM 有序、可靠、双向的面向连接字节流
SOCK_DGRAM和SOCK_STREAM这两种类型的是在应用中使用较频繁的两种套接字类型。前者是基于无连接的,即UDP传输,后续是基本面向连接的,即TCP传输。
字节序:
在进行socket通信的时候,需要注意字节序的类型。通常的计算机中存在着两种不同的字节序:大端字节序和小端字节序。这两种字节序的主要区别在于最大字节地址和数字最低有效字节的关系。大端字节序是最大字节地址对应最低有效字节,反之既是小端字节序。因此当用一个指针cp指向保存着0x04030201的内存时,cp[0] = 4 而cp[3] = 0,小端字节序则是cp[0] = 0, cp[3] = 4。
在TCP/IP协议中所以的地址都是大端模式的,而linux系统的地址则通常是小端模式的。因此需要有相关的函数进行转化:
uint32_t htonl(uint32_t hostint32) 32位主机地址装成网络地址格式
uint16_t htonl(uint16_t hostint16) 16位主机地址装成网络地址格式
uint32_t ntohl(uint32_t netint32) 32位网络地址装成主机地址格式
uint16_t ntohl(uint16_t netint16) 16位网络地址装成主机地址格式
主机地址和端口号
在socket通信中,一个进程通常需要绑定自身的IP地址来告知想要与其进行通信的另一进程发送请求的目的地,以及一个端口号来告知接受请求的计算机中对应的进程。因此建立socket通信必须提供对应的主机地址和端口号。
在IPv4中主机地址是32位的,但是为了记忆方便我们通常将其表示为十进制字符串(192.168.1.1),linux提供了两个接口用于二进制表示和十进制字符串表示的转换。
const char *inet_ntop(int domain, void *restrict addr, char *restrict str, socklen_t size)
成功返回字符串表达式的地址指针,失败NULL
int inet_pton(int domain, const char *restrict str, void *restrict addr),
成功返回1,格式无效返回0,若出错返回-1。Addr中存放转换之后的网络地址
系统中0到1024的端口号是为特定的服务预留的(ftp 21, http 80),如果想要使用则要有超级用户的权限。通常我们使用的端口号是从1025开始的。
主机名和服务:
Socket通信的建立需要提供地址和端口号,但是我们也可以使用主机名和服务名来建立socket通信。通过调用getaddrinfo函数可以将主机名和服务名字映射到一个称为struct addrinfo的结构内部。
int getaddrinfo(const char *restrict host, const char *restrict service,
const char *restrict hint, struct addrinfo **restrict res)
成功返回0,失败返回-1。.
该函数需要用户输入主机名host,服务名service,以及筛选信息hint,返回之后可以在res中得到可以用于bind以及connect接口的地址。(struct addrinfo 的具体信息感兴趣的同学请查阅unix手册)
三、 数据结构
在使用socket的接口函数时会遇到一些保存socket通信所需信息的数据结构。其中最重要的一个结构是sockaddr,定义如下:
struct sockaddr
{
sa_family_t sa_family; /*address family*/
char sa_data[ ]; /*variable-length address */
};
该结构是众多socket接口函数的主要参数类型,许多传入接口函数的变量,将被强制转换成该结构。在IPv4因特网域中(AF_INET),套接字用如下数据结构表示(linux中的定义):
struct sockaddr_in
{
sa_family_t sin_family; /*address family */
in_port_t sin_port; /*port number */
struct in_addr sin_addr; /*IPv4 address */
unsigned char sin_zero[8]; /*filler */
};
当用户将该数据结构类型的变量传给struct sockaddr类型的变量的接口函数的时候,都需要强制转换为struct sockaddr类型。
四、 socket编程步骤
需要socket通信的进程之间的关系,可以看成server与client的关系。在一台设备上运行一个server进程,同时需要在另一台主机上运行一个或多个client进程。为了使client进程可以和server进程通信,我们需要进程一下步骤的编程:
客户端:
1)建立socket描述符。socket描述符与文件描述符类似,是后续socket接口操作的入口。事实上read和write也可以操作socket描述符。
#include
int socket(int domain, int type, int protocol)
返回值:成功返回socket描述符,失败返回 -1
备注:domain和type既是前面说过的域和套接字类型。Protoclo是选择的协议,通常我们使用0来设置默认协议。AF_INET中SOCK_DGRAM默认的协议是UDP,SOCK_STREAM默认的协议是TCP。
2)为struct sockaddr_in类型的变量成员赋值,该变量是后续建立连接调用connect函数的参数。主要包括地址族sa_family,端口号port以及地址sin_addr。
3)调用connect接口与服务器进程建立连接。默认情况下Connet函数通常会阻塞直到服务器接受该连接后返回。
int connect(int sockfd, struct sockaddr *adr, socklen_t len)
成功返回0,失败返回-1。
4)建立连接之后使用通信接口函数实现进程之间的数据通信。有四对接口用于发送与接收数据。
ssize_t read(int fd, void*buf, size_t count);
ssize_t write(int fd, void*buf, size_t count);
read和write的使用和文件操作是一样的,只是第一个参数换成socket描述符。
ssize_t send(int sockfd, const void*buf, size_t nbytes, int flags)
ssize_t recv(int sockfd, const void* buf, size_t nbytes, int flags)
这两个函数的使用和read/write函数对类似,只是多了一个flags标志,该标志用来改变传输数据的方式。(有兴趣的同学查阅unix手册)
以上两个函数是用于面向连接的STREAM类型的套接字,还有与这两个函数很相似的sendto函数和recvfrom函数可以用于无连接的DGRAM类型套接字。
ssize_t sendto(int sockfd, const void*buf, size_t nbytes, rint flags,
const struct sockaddr *destaddr, socklen_t destlen);
ssize_t recvfrom(int sockfd, const void* buf, size_t nbytes, int flags,
struct sockaddr *addr, socklen_t *addrlen)
在sendto函数中需要指定发送的目的地址,以及目的地址的长度。因为是无连接 的套接字,所以要在发送的程序中指定目标地址。在recvfrom函数中addr保存的是发送程序所在计算机的地址,addrlen中保存地址的长度。
最后还有一对用于不连续存储缓冲区的发送接收函数:sendmsg和recvmsg
Ssize_t sendmsg(int sockfd, const struct msghdr *mag, int flag)
Ssize_t recvmsg(int sockfd, const struct msghdr *mag, int flag)
成功返回以字节计数的消息长度,若无可用消息或对方已经按序结束则返回0,失败返回-1;
服务器:
1) 建立socket描述符。和客户端一样
2) 绑定服务器的IP地址以及端口号。该步骤可以调用bind函数实现,但是在调用bind函数之前通常需要将主机地址和端口号赋给类型为struct sockaddr_in的变量addr中的相应成员。
int bind(int sockfd, const struct sockaddr *addr, socklen_t len)
成功则返回0,出错返回-1
3) 调用listen函数宣告可以接受连接请求
int listen(int sockfd, int backlog)
成功返回0,失败返回-1,backlog是系统允许进入等待主机响应的队列的数量
4) 调用accpet函数建立连接
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len)
成功返回套接字描述符,失败返回-1
备注:返回的套接字描述符叫做连接描述符,参数sockfd叫做监听描述符。真正和客户端进程建立连接的套接字是返回的连接套接字,后续的发送和接受操作也是在该套接字上进行的。监听套接字描述符可以继续监听后续达到的连接请求。
5)调用发送接收函数进行数据通信,和客户端中的最后一步一样,有四组接口可以调用。
示例程序
client.c
server.c