Unix环境高级编程(APUE)中介绍了套接字socket的使用,本文从开发者使用过程角度简单介绍了服务器开启监听、客户端发起连接、子线程创建的一些过程以及Unix中套接字的地址格式等内容。
连接过程
创建套接字地址
- 套接字地址结构
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 */ };
- 创建套接字地址
sockaddr_in
(IPv4),sockaddr_in6
(IPv6) - 初始化套接字地址,以IPv4为例
sin_family
表示族,可选IPv4 -AF_INET
或 IPv6 -AF_INET6
sin_port
地址对于的端口,应大于1024,否则需要superuser权限sin_addr
网络地址的二进制表示,通过预定义的值如本地环回INADDR_LOOPBACK
、任意INADDR_ANY
设置,或通过inet_aton
函数将点分十进制(127.0.0.1)格式转换为所需格式,由于网络字节序与主机字节序可能不同,因此会再使用htonl
,htons
,ntohl
,ntohs
等函数进行转换。sin_zero
Linux中定义的填充字节,为0
- 使用套接字地址
bind
,connect
等
服务器开启监听
创建套接字地址及套接字,绑定,开启监听。其中开启监听在收到请求之前会阻塞(block),请求到来后会返回分配的新套接字描述符。
- 创建套接字地址,如上文所述
- 创建套接字
socket(int domain, int type, int protocol)
,返回套接字描述符, -1 on errordomain
指定域,如IPv4 -AF_INET
或 IPv6 -AF_INET6
type
指定连接类型,有如下四种SOCK_DGRAM
定长、无连接、不可靠报文,默认 UPDSOCK_RAW
IP数据报接口SOCK_SEQPACKET
定长、有序、可靠、面向连接的报文,需要AF_UNIX
domainSOCK_STREAM
定长、可靠、双向、面向连接的数据流,默认 TCP
protocol
协议,0为默认,其它可选IPPROTO_IP
,IPPROTO_IPv6
,IPPROTO_TCP
,IPPROTO_UDP
- 绑定套接字到指定地址
bind(int sockfd, const struct sockaddr *addr, socklen_t len)
,0 OK, -1 on errorsockfd
套接字描述符addr
套接字地址,需要将sockaddr_in
类型转换为此类型(转换指针的类型)len
地址的长度
- 开启监听
listen(int sockfd, int backlog)
,0 OK, -1 on errorsockfd
如前所述backlog
等待队列中的请求个数
代码
#define SVR_PORT 2300
#define SVR_BACKLOG 10
// listen
void start_listen() {
// init socket addr
sockaddr_in svr_addr;
memset(&svr_addr, 0, sizeof(sockaddr_in));
svr_addr.sin_family = AF_INET, svr_addr.sin_port = htons(SVR_PORT), svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// init socket to listen
int sd_li = socket(AF_INET, SOCK_STREAM, 0);
if (sd_li == -1)
err_exit(-1, "socket: create socket failed.");
// bind socket to addr
int err = bind(sd_li, (sockaddr *)&svr_addr, sizeof(svr_addr));
if (err != 0)
err_exit(err, "bind: bind failed.");
// listen
err = listen(sd_li, SVR_BACKLOG);
if (err == -1)
err_exit(err, "listen: listen failed.");
printf("start listen on port %d\n", SVR_PORT);
// accept client request
while (1) {
sockaddr_in addr_peer;
socklen_t len_peer;
int sd_acc = accept(sd_li, (sockaddr *)&addr_peer, &len_peer);
if (sd_acc == -1)
err_exit(sd_acc, "accept: accept client error.");
printf("connected to client %xd.\n", addr_peer.sin_addr.s_addr);
// start a new thread and send hello back
pthread_t subthread;
err = pthread_create(&subthread, NULL, client_thread_fn, (void *)sd_acc);
if (err != 0)
err_exit(err, "pthread_create: create sub thread error.");
}
}
客户端连接建立
- 创建套接字地址及套接字,如上文所述
- 连接
connect(int sockfd, const struct sockaddr *addr, socklen_t len)
, 0 if OK, -1 on erorsockfd
如前所述addr
套接字地址len
地址长度
- 关闭套接字
close
,该方法实际上是关闭了套接字(文件)描述符,因此需要包含unistd.h
头文件
代码
#define SVR_HOST_STR "127.0.0.1"
// connect to server, return socket descriptor if success
int connect() {
// init socket
sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET, addr.sin_port = htons(SVR_PORT);
inet_aton(SVR_HOST_STR, &addr.sin_addr);
// create socket
int sd = socket(AF_INET, SOCK_STREAM, 0);
if (sd == -1)
err_exit(sd, "socket: create socket failed.");
// connect
int ret_conn = connect(sd, (sockaddr *)&addr, sizeof(addr));
if (ret_conn != 0)
err_exit(ret_conn, "connect: connect failed.");
return sd;
}
套接字地址格式
由于不同的domain地址格式不同,因此Unix定义了统一的格式 sockaddr
用来内部使用,而开发者则针对不同的domain使用特定的格式,如 sockaddr_in
和 sockaddr_in6
,使用时需要将这些domain特定的格式转换为 sockaddr
格式。以下为Linux下这些格式的定义:
struct sockaddr {
sa_family_t sa_family; /* address family */
char sa_data[]; /* variable-length address */
};
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 in_addr {
in_addr_t s_addr; /* IPv4 address */
};
上述 sockaddr_in
格式中的 in_addr
表示因特网地址(二级制或者说整数形式,不同于点分十进制 127.0.0.1)。当然,Unix提供了 inet_ntop
和 inet_pton
两个函数,实现点分十进制与二进制之间的转换。前者将二进制转换为点分十进制,后者将点分十进制转换为二进制。其中点分十进制为 char *restrict str
字符串,size
指字符串缓冲区的长度;二进制为 void *restrict addr
。
const char *inet_ntop(int domain, const void *restrict addr, char *restrict str, socklen_t size);
int inet_pton(int domain, const char *restrict str, void *restrict addr);
如前所述,由于网络字节序Byte Order与主机字节序可能不同,因此Unix提供了下列四个函数用来转换,其中h
表示主机host
,n
表示network
,l
和s
分别表示long
和short
:
uint32_t htonl(uint32_t hostint32);
Returns: 32-bit integer in network byte order
uint16_t htons(uint16_t hostint16);
Returns: 16-bit integer in network byte order
uint32_t ntohl(uint32_t netint32);
Returns: 32-bit integer in host byte order
uint16_t ntohs(uint16_t netint16);
Returns: 16-bit integer in host byte order
数据传输
数据传输过程主要为发送和接收数据两种,两种各包含四个相似函数可以使用,这里主要介绍其中第一种,即 send
和 recv
,如下:
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
前三个参数不难理解,分别是套接字描述符、待发送/接收消息缓冲区,缓冲区长度,最后一个是标志。一般为0即可,其它值如 MSG_OOB
, MSG_PEEK
等表示不同含义,此处不一一列出。
线程创建
线程创建主要使用 pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
函数,注意,g++需要在编译时加上参数 -lpthread
才能使用相关函数。 attr
为属性,一般设置为 NULL
即可,start_routine
函数为子线程的开始函数,arg
为其参数。子线程创建后会从该函数开始执行。
代码
// thread function
void *thr_fn(void *x) {
//...
}
int main{
int sd = 1; // para
pthread_t sub_thr;
int err = pthread_create(&sub_thr, NULL, thr_fn, (void *)sd);
if (err != 0)
err_exit(err, "create sub thread failed.");
}