Zach的博客

Unix套接字编程基础

数据结构

这里仅讨论IPv4相关的数据,IPv6与之类似,可以查阅相关的资料。

网络套接字的地址结构:

1
2
3
4
5
#include <netinet/in.h>

struct in_addr {
in_addr_t s_addr; // 32位IPv4地址
}
1
2
3
4
5
6
7
8
9
#include <netinet/in.h>

struct sockaddr_in {
uint8_t sin_len; // 结构的长度
sa_family_t sin_family; // AF_INET,地址族
in_port_t sin_port; // 端口号

char sin_zero[8]; // unused
}

网络协议规定必须指定一个网络字节序(大端还是小端),作为网络编程人员必须清楚不同字节之间的差异。比如,在每一个TCP分节中都有16位的端口号和32位的IPv4地址,发送协议栈和接收协议栈必须就这些多字节字段各个字节的传送顺序达成一致。网络协议使用大端字节序来传送这些多字节数据。

举例来说,如果有一个32位的IPv4地址,首先要转换字节序再使用。

1
uint32_t ip_addr = htonl(ori_ip_addr);

共有4个转换函数:

1
2
3
4
5
6
7
8
9
#include <netinet/in.h>

uint16_t htons(uint16_t);

uint32_t htonl(uint32_t);

uint16_t ntohs(uint16_t);

uint32_t ntohl(uint32_t);

h表示host,n表示network,s表示short,l表示long

通常来说我们习惯用点分十进制来表示IP地址,如192.168.1.1,因为这样易读易懂,但是在网络上传输时应该使用的32位的二进制来表示IP地址,于是就要进行转换,以下两个函数提供转换功能:

1
2
3
4
5
#include <arpa/inet.h>

int inet_pton(int family, const char *strptr, void *addrptr);

const char *inet_ntop(int family, const char *addrptr, char *strptr, size_t len);

family即可以是AF_INET,也可以是AF_INET6。如果是不被支持的地址族作为family参数,返回一个错误,errno被设置为EAFNOSUPPORT

这里n表示numeric,p表示presentation。

基本TCP套接字编程

sock函数

首先需要创建一个套接字,创建套接字的函数如下:

1
2
3
#include <sys/socket.h>

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

family指定协议族,type指定套接字类型,protocol指定某个协议类型常值,当设置为0时,程序会选择和给定的family和type组合的系统默认值。

connect函数

TCP客户端用connect函数来与TCP服务器建立连接。

1
2
3
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

客户不必在使用connect前调用bind函数,因为需要的话内核会分配一个临时的端口给客户进程。

出错返回的错误情况:

  1. TCP客户等待一段时间后没有收到服务器返回的TCP分节,返回ETIMEDOUT错误。
  2. 对客户的SYN响应是RST(复位),表明服务器上并没有进程在等待与之连接,这是一种硬错误,客户收到后马上返回ECONNREFUSED。
  3. 客户发出的SYN在某个路由上引发了一个”destination unreachable”的ICMP错误,则认为是一种软错误。客户机内核保存该消息,并持续发送SYN包,如果等待一段时间后未收到响应,就把保存的信息(ICMP错误)作为EHOSTUNREACH或ENETUNREACH错误返回给进程。

bind函数

绑定一个本地协议地址给一个套接字,对于网际协议,协议地址是32位的IPv4地址或128位的IPv6地址与16位的TCP或者UDP端口号的组合。

函数原型:

1
2
3
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

listen函数

listen仅由TCP服务器调用,它把一个未连接的套接字转换成一个被动套接字,以接收客户的请求。

1
2
3
#include <sys/socket.h>

int listen(int sockfd, int backlog);

内核为任何一个给定的监听套接字维护两个队列:

  1. 未完成连接队列:客户已经发送一个SYN分节给服务器,而服务器正在等待完成对应的三次握手过程。
  2. 已完成连接队列:每个已完成TCP三次握手的客户对应启动一项。

backlog指定两个队列总和的最大值。

要注意的是,在三次握手完成之后,但在服务器调用accept之前到达的数据应由服务器TCP排队。

accept函数

accept函数用于TCP服务器调用,将从已完成队列返回队头的连接。如果已完成连接队列为空,那么服务器进程将被睡眠,直到下一个连接完成。

函数原型:

1
2
3
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t, *addrlen);

一个并发的服务器的例子

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
27
28
29
30
31
32
33
34
35
36
37
38
#include "unp.h"
#include <time.h>
#include <string.h>

int main(int argc, char **argv) {
int listenfd, connfd;
socklen_t len;

struct sockaddr_in servaddr, cliaddr;
char buf[MAXLINE];
time_t ticks;
pid_t pid;

listenfd = Socket(AF_INET, SOCK_STREAM, 0);
memset(servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(13);

Bind(listenfd, (SA* )&servaddr, sizeof(servaddr));

Listen(listenfd, LISTENQ);

while (1) {
len = sizeof(connfd);
connfd = Accept(listenfd, (SA *)&cliaddr, &len);
if ((pid = fork()) == 0) {
// 子进程
Close(listenfd);
do_process(connfd);
Close(connfd);
exit(0);
}
Close(connfd);
}

return 0;
}