Linux 套接字编程基础

创建套接字

套接字需要通过调用 socket() 函数创建。如果创建成功,socket() 函数的返回值就是套接字的文件描述符,否则是 -1

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

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

第一个参数用于说明 地址族。通信的前提就是能够定位另一方。如果找不到另一方,通信就无从谈起。地址族取决于通信的情形,不同的通信情形对应不同的地址族。

进程间通信的情形大致可分为:本地的两个进程基于文件系统进行通信;两个进程通过 TCP/IP 体系结构的网络进行通信。其中 TCP/IP 有 IPv4 和 IPv6 两种地址族。

domain 取值 含义
AF_UNIXAF_LOCAL 本地通信
AF_INET IPv4
AF_INET6 IPv6

AF 即 Address Family,INET 即 Internet

后面两个参数分别说明套接字的类型和协议。对于 TCP/IP,类型是和协议一一对应的。

type 取值 含义
SOCK_STREAM 面向字节流,用于 TCP 协议
SOCK_DGRAM 面向数据报,用于 UDP 协议
SOCK_RAW 原始套接字,用于 IP、ICMP 等底层协议

从 Linux 内核 2.6.17 起,参数 type 还可以接受上述取值与 SOCK_NONBLOCKSOCK_CLOEXEC 按位或的值。前者表示将 Socket 设为非阻塞;后者表示子进程中关闭该 Socket

protocol 取值 含义
IPPROTO_TCP TCP 协议
IPPROTO_UDP UDP 协议

在程序中调用 socket() 函数创建一个使用 IPv4 协议族、使用面向字节流的 TCP 协议的套接字,套接字的文件描述符用一个整型变量 server_sockfd 保存。

1
2
3
4
5
int server_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (server_sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}

如果要使用的是 UDP 协议,则参数应改为:

1
server_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

应该在 socket() 函数之后使用一个 if 语句。这样才能在套接字创建失败时及时发现。使用 exit() 函数需要包含头文件 stdlib.h

1
#include <stdlib.h>

fd 即 file descriptor 文件描述符

套接字的地址

确定了套接字的地址族、类型和协议后,还要给出套接字要关联的地址,也就是给出程序所在主机能与外界通信的地址以及希望分配给进程的端口号。

地址结构

套接字的地址用一个结构来组织。对于 IPv4,地址结构的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// netinet/in.h
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */

/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};

sin 即 sockaddr_in,in 即 Internet

第一个成员实际上是名为 sin_family 的无符号短整型,含义同 socket() 函数的第一个参数,即地址族。

1
2
3
4
5
6
7
// netinet/in.h > sys/socket.h > bits/socket.h > bits/sockaddr.h
typedef unsigned short int sa_family_t;

#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family

#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))

第二个成员是 16 位的无符号短整型,用来保存端口号。

1
2
3
4
5
// stdint.h
typedef unsigned short int uint16_t;

// netinet/in.h
typedef uint16_t in_port_t;

第三个成员是一个 in_addr 类型的结构,仅有一个 32 位的无符号整型成员,IP 地址就保存于此。

1
2
3
4
5
6
7
8
9
10
// stdint.h
typedef unsigned int uint32_t;

// netinet/in.h
typedef uint32_t in_addr_t;

struct in_addr
{
in_addr_t s_addr;
};

第四个成员的存在只是为了使整个结构的长度与结构 sockaddr 保持一致。本身没有实际意义,用 0 填充即可。

在程序中定义一个 sockaddr_in 类型的结构变量 server_addr,用 memset() 函数将其全部填 0 后,给 sin_family 成员赋值。

1
2
3
4
struct sockaddr_in server_addr;

memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;

有专门的清 0 函数 bzero()

1
bzero(&server_addr, sizeof(struct sockaddr_in));

使用 memset() 函数或 bzero() 函数都需要包含头文件 string.h

1
2
3
4
#include <string.h>

void *memset(void *s, int c, size_t n);
void bzero(void *s, size_t n);

网络字节序

在填写 IP 地址和端口号时,须将其转换到合适的字节序。一个整型由若干个字节构成,在内存中连续占用若干个存储单元。大多数 CPU 都采用 小端字节序:把最低位字节存于占用的第一个存储单元,而把次低位字节存于下一个存储单元。例如,只有两个字节的短整型 0xff00,若低位字节 00 存于 n,则高位字节 ff 存于 n+1

若反过来,把最高位字节存于占用的第一个存储单元则称为 大端字节序

字节在内存中的存储顺序,称为 主机字节序;字节在网络中的传输顺序称为 网络字节序。TCP/IP 规定,最高位字节先传输,次高位字节次之,最低位字节最后传输。为此,须确保 CPU 存取字节的顺序和 TCP/IP 规定的网络字节序相同,即保证主机字节序是大端字节序。

头文件 arpa/inet.h 提供了一组函数,可完成主机字节序到网络字节序的转换。

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

// 从主机字节序到网络字节序
uint32_t htonl(uint32_t hostlong) // 适用于 IPv4 地址
uint16_t htons(uint16_t hostshort) // 适用于端口号

// 从网络字节序到主机字节序
uint32_t ntohl(uint32_t netlong) // 适用于 IPv4 地址
uint16_t ntohs(uint16_t netshort) // 适用于端口号

htons 即 host to network short,htons 即 host to network long

在程序中,借助 htons() 函数为结构变量 server_addr 赋予 IP 地址和端口号。

1
2
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = INADDR_ANY;

INADDR_ANY 是一个全 0 的 IP 地址常量,在头文件 netinet/in.h 中定义。

1
2
// netinet/in.h
#define INADDR_ANY ((in_addr_t) 0x00000000)

全 0 的 IP 地址表示本机拥有的任意 IP 地址。

IP 地址通常采用点分十进制表示,如 "127.0.0.1"。对于这种形式的 IP 地址,inet_addr() 函数可将其解析为网络字节序。

1
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

用于处理 IP 地址的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <arpa/inet.h>

// 将一个点分十进制表示的 IP 地址转换成网络字节序,
// 结果作为返回值
in_addr_t inet_addr(const char *cp);

// 将一个点分十进制表示的 IP 地址转换成网络字节序,
// 结果存储在 inp 指向的 in_addr 结构中
int inet_aton(const char *cp, struct in_addr *inp);

// 获取一个 in_addr 结构中的 IP 地址的点分十进制表示
// 结果作为返回值
char *inet_ntoa(struct in_addr in);

与描述符绑定

客户端程序通常不进行此步骤,这种情况下,客户端程序可在下一步和服务器建立连接后,通过调用 getsockname() 函数获取由系统自动分配的地址和端口号。

bind() 函数将地址结构与描述符绑定。

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

int bind(int fd, const struct sockaddr * addr, socklen_t addr_len)
  • fd 要与之绑定的描述符;
  • addr 一个 sockaddr 结构的地址。在 IPv4 中,用来保存地址的结构是 sockaddr_in 类型,而不是 sockaddr 类型。因此,对其取址后,还要进行强制类型转换;
  • addr_len 前一个参数的大小。

如果绑定成功,bind() 函数将返回 0,否则返回 -1

在程序中将结构 server_addr 与描述符 server_sockfd 绑定。

1
2
3
4
if (bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}

要检查 bind() 函数的返回值,这样一旦绑定失败,就能立即知道。

如果服务器程序在调用 bind() 函数之前,没有设置端口号,或者说设置为 0,那么在调用 bind() 函数时,内核就会从空闲的端口号中随机挑选一个,作为进程在此次通信中所使用的端口。这种情况下,通过 getsockname() 函数可以得到确切的端口号。

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

int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

函数 getsockname() 会将套接字 sockfd 绑定的地址信息写入 addr 指向的地址结构。第三个参数 addrlen 指向的 socklen_t 型变量在调用前,必须初始化为地址结构的大小,函数返回时,该变量会被设为地址信息的实际大小。

如果提供的地址结构太小,地址将被截断。在这种情况下,addrlen 指向的变量会被设定为一个超出正常范围的值。

程序在调用 bind() 函数之后,可检查端口号,如果是 0,则调用 getsockname() 函数以获取确切的值。

1
2
3
4
5
if (server_addr.sin_port == 0) {
socklen_t server_addrlen = sizeof(struct sockaddr_in);
getsockname(server_sockfd, (struct sockaddr *)&server_addr, &server_addrlen);
printf("local port: %hu\n", ntohs(server_addr.sin_port));
}

函数 getsockname() 获取的是本机的 IP 地址和端口号。

建立连接

为套接字绑定好地址后,如果是面向字节流的套接字,需要建立连接才能进行通信。如果是面向数据报的套接字,则不需要。

服务器接受并处理连接

对于服务器程序来说,须调用 listen() 函数通知底层协议开始接收来自客户端的请求,并与之建立连接,即开始 监听。陆陆续续建立的连接将形成一个队列,等待 accept() 函数调取。

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

int listen(int fd, int n)
  • fd 要开始监听的套接字的描述符;
  • n 队列的最大长度。如果连接数达到这个值,往后的请求将被直接拒绝。比如设为 5,也可以使用常量 SOMAXCONN,这将由系统决定队列的最大长度。

如果顺利,listen() 函数将返回 0,否则返回 -1

在程序中启动对套接字 server_sockfd 的监听。

1
2
3
4
if (listen(server_sockfd, SOMAXCONN) == -1) {
perror("listen");
exit(EXIT_FAILURE);
}

接下来要做的就是调用 accept() 函数。

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

int accept(int fd, struct sockaddr * addr, socklen_t * addr_len)

accept() 函数会等到有客户端接入时才返回。返回值是一个新的文件描述符。后续与该客户端的通信都要通过这个文件描述符进行。

accept() 函数还会将客户端的地址信息存放到第二个参数指向的地址结构,并在第三个参数指向的整型变量给出地址信息的实际长度。

调用 accept() 函数时,如果对客户端的地址信息感兴趣,必须将第三个参数指向的整型变量设为可接受的长度,也就是地址结构的长度;如果不感兴趣,将第二、三个参数均设为 NULL 即可。

在程序中调用 accept() 函数,处理套接字 server_sockfd 的队列。

1
2
3
4
5
6
7
8
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof (struct sockaddr_in);

int client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_sockfd == -1) {
perror("Failed to accept a new connection on a socket");
exit(EXIT_FAILURE);
}

客户端请求建立连接

对于客户端来说,是调用 connect() 函数主动去连接服务器的套接字。

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

int connect(int fd, const struct sockaddr * addr, socklen_t addr_len);
  • fd 用来与服务器通信的套接字的描述符;
  • addr 指向一个包含服务器地址信息的地址结构;
  • addr_len 第二个参数的大小。

如果顺利,connect() 函数将返回 0,否则返回 -1

在程序中调用 connect() 函数尝试与 server_addr 指定的服务器程序建立连接。

1
2
3
4
5
6
7
if (connect(server_sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in)) == -1) {
fprintf(stderr, "Failed to connect to %s port %hu: %s\n",
inet_ntoa(server_addr.sin_addr),
ntohs(server_addr.sin_port),
strerror(errno));
exit(1);
}

调用 connect() 函数之后,客户端可调用 getsockname() 函数获取进程在此次通信中所使用的端口号,以及本机的 IP 地址。

1
2
3
4
5
6
struct sockaddr_in local_addr;
socklen_t local_addrlen = sizeof(struct sockaddr_in);
getsockname(server_sockfd, (struct sockaddr *)&local_addr, &local_addrlen);

printf("local address: %s\n", inet_ntoa(local_addr.sin_addr));
printf("local port: %hu\n", ntohs(local_addr.sin_port));

传输字节流

TCP 连接一旦建立,不管是服务器,还是客户端,都会开辟一个接收缓存和发送缓存。发送缓存的内容是即将发送给对方的数据;接收缓存的内容是来自对方的数据。

函数 recv() 从接收缓存读取指定数量的字节; send() 函数将指定数量的字节写入发送缓存。

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

ssize_t recv(int fd, void *buf, size_t n, int flags);
ssize_t send(int fd, const void *buf, size_t n, int flags);

这两个函数的第一个参数 fd 都要求一个描述符,以说明通过哪个套接字收发字节。

recv() 函数从第二个参数 buf 的指向开始,写入从接收缓存读取到的字节;send() 函数从第二个参数 buf 的指向开始,读取字节到发送缓存。它们的第三个参数 n 都用于指定读写的字节数。

对于 send() 函数,默认在发送完所有字节才返回;对于 recv() 函数,默认只要接收到字节就返回,哪怕只有一个字节。它们的最后一个参数 flags 就是用来改变这种默认行为的,一般设为 0,表示保持默认。

如果读写成功,它们的返回值都是实际读写的字节数,出错则返回 -1。如果对端关闭连接,recv() 函数返回 0send() 函数则返回 -1,同时将 errno 设为 ECONNRESET

在程序中调用 send() 向套接字 server_sockfd 发送字节。第二个参数给出了要发送的字节 "123456789",但第三个参数指定只发送 5 个字节,因此另一方只能接收到 "12345"

1
2
3
4
5
buflen = send(server_sockfd, buf, strlen(buf), 0);
if (send(server_sockfd, "123456789", 5, 0) == -1) {
perror("send");
exit(EXIT_FAILURE);
}

do-while 循环中调用 recv() 函数从套接字 client_sockfd 读取字节到字符数组 buf,读取到的字节数保存在整型变量 buflen 中,用 printf() 函数输出读取到的字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define BUF_SIZE 140

int buflen;
char buf[BUF_SIZE];

do {
buflen = recv(client_sockfd, buf, BUF_SIZE, 0);
if (buflen > 0) {
if (buf[buflen - 1] != '\0')
buf[buflen < BUF_SIZE ? buflen : buflen - 1] = '\0';
printf("Received a message: %d: %s", buflen, buf);
} else if (buflen == 0) {
printf("Connection closed by foreign host.\n");
break;
} else if (buflen == -1) {
perror("send");
exit(EXIT_FAILURE);
}
} while (strncasecmp(buf, "quit", 4) != 0);

当接收到 "quit" 时,do-while 循环的条件就不成立,便跳出循环。

参数 flags 的取值是一些宏,这些宏可以同时起作用,只需将它们进行按位或运算即可。

flags 取值 含义
MSG_DONTWAIT 不要阻塞,立即返回。若没有数据可读写,仍然返回 -1,同时 errno 会被设置为 EAGAINEWOULDBLOCK
MSG_OOB 发送或接收紧急数据
MSG_PEEK 不要将本次读取到的数据从接收缓存中清除
MSG_WAITALL 必须读取到指定数量的字节才返回,只能在阻塞模式下使用
MSG_NOSIGNAL 在进行写操作时,如果中途对端关闭读,不要引发 SIGPIPE 信号

收发数据报

如果是 UDP 套接字,客户端在调用 socket() 函数之后,可直接调用 sendto() 函数向服务器发送数据报;服务器在调用 bind() 函数之后,可以调用 recvfrom() 函数开始接收来自客户端的数据报。

由于 UDP 是面向无连接的,每次发送数据报都要给出目标地址;接收数据报时也要同时获取源地址,否则无法知道收到的数据报来自哪个客户端。

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

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

关闭连接

不管是 TCP 套接字还是 UDP 套接字,要结束通信,必须调用 close() 函数来关闭连接。

1
2
3
#include <unistd.h>

int close(int fd);

实际上,close() 并不会立即把连接关闭,它只是把 fd 的引用次数减 1。只有当引用次数为 0 时,才真正关闭连接。在多进程程序中,每创建一个子进程,就会使父进程中打开的 Socket 的引用次数加 1

如果想要立即关闭连接,则应该使用下面函数。

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

int shutdown(int fd, int howto);

参数 howto 决定 shutdown() 的行为,取值如下:

  • SHUT_RD 不能再进行读操作,已经在接收缓存中的数据会被丢弃
  • SHUT_WR 不能再进行写操作,已经在发送缓存中的数据会在真正关闭连接之前发送出去
  • SHUT_RDWR 不能再进行读操作,也不能再进行写操作

附录 A:TCP 服务器程序

该服务器程序启动后,将在 0.0.0.0:8080 上监听,并将接收到的消息输出。如果收到 "quit" 则关闭连接并结束退出。源码如下:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <stdio.h>
#include <stdlib.h> // exit()
#include <stdint.h> // uint8_t
#include <string.h> // memset(), memcpy(), strcpy(), strerror()
#include <unistd.h> // close()
#include <sys/socket.h> // socket(), bind(), listen(), accept(), recv(), send()
#include <arpa/inet.h> // htons(), htons(), inet_addr()
#include <errno.h> // errno

#define BUF_SIZE 140

int main()
{
int server_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (server_sockfd == -1) {
perror("Faild to create socket.");
exit(1);
}

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = INADDR_ANY;

if (bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in)) == -1) {
perror("Faild to bind socket.");
exit(1);
}

if (server_addr.sin_port == 0) {
socklen_t server_addrlen = sizeof(struct sockaddr_in);
getsockname(server_sockfd, (struct sockaddr *)&server_addr, &server_addrlen);
printf("local port: %hu\n", ntohs(server_addr.sin_port)); // %hu 用于显示无符号短整型
}

if (listen(server_sockfd, SOMAXCONN) == -1) {
perror("Faild to listen socket.");
exit(1);
}

int client_sockfd = accept(server_sockfd, NULL, NULL);
if (client_sockfd == -1) {
perror("Failed to accept a new connection on a socket.");
exit(1);
}

int buflen;
char buf[BUF_SIZE];

do {
buflen = recv(client_sockfd, buf, BUF_SIZE - 1, 0);
if (buflen > 0) {
buf[buflen] = '\0';
printf("Received a message: %d: %s", buflen, buf);
} else if (buflen == 0) {
printf("Connection closed by foreign host.\n");
break;
} else if (buflen == -1) {
fprintf(stderr, "Failed to receive bytes from socket %d: %s\n", client_sockfd, strerror(errno));
exit(1);
}
} while (strncasecmp(buf, "quit", 4) != 0);

close(client_sockfd);
close(server_sockfd);
return 0;
}

用 vi 命令将以上源码保存在名为 tcp_server.c 的文件中,用 gcc 命令编译生成名为 tcp_server 的目标文件并运行。

1
2
3
4
$ vi tcp_server.c
$ gcc tcp_server.c -o tcp_server
$ ./tcp_server

这时程序将在终端中持续运行。在另一个终端中,用 netstat 命令可以看到程序占用了 8080 端口,用 telnet 命令与之建立连接。

1
2
3
4
5
6
7
$ netstat -antp | grep tcp_server
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 3150/./tcp_server
$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.

若发送 Hello World!,可以看到在上一个终端运行的服务器程序输出 Hello World!

1
2
3
4
5
6
$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Hello World!

1
2
3
$ ./tcp_server 
Received a message: 14: Hello World!

发送 quit 则断开连接。

1
2
3
4
5
6
7
$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Hello World!
quit
Connection closed by foreign host.
1
2
3
$ ./tcp_server 
Received a message: 14: Hello World!
Received a message: 6: quit

若不发送 quit,而是按 Ctrl + ],再输入 quit 则程序提示对端关闭连接。

1
2
3
4
5
6
7
8
9
$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Hello World!
^]

telnet> quit
Connection closed.
1
2
3
$ ./tcp_server 
Received a message: 14: Hello World!
Connection closed by foreign host.

附录 B:TCP 客户端程序

该客户端程序启动后,会尝试连接到 127.0.0.1:8080,并将终端输入的字符串发送过去。如果发送的是 "quit" 则关闭连接并退出。源码如下:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <stdio.h>
#include <stdlib.h> // exit()
#include <stdint.h> // uint8_t
#include <string.h> // memset(), memcpy(), strcpy(), strerror()
#include <unistd.h> // close()
#include <sys/socket.h> // socket(), bind(), listen(), accept(), recv(), send()
#include <arpa/inet.h> // htons(), htons(), inet_addr()
#include <errno.h> // errno

#define BUF_SIZE 140

int main()
{
int server_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (server_sockfd == -1) {
perror("Faild to create socket.");
exit(1);
}

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

if (connect(server_sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in)) == -1) {
fprintf(stderr, "Failed to connect to %s port %hu: %s\n",
inet_ntoa(server_addr.sin_addr),
ntohs(server_addr.sin_port),
strerror(errno));
exit(1);
}

struct sockaddr_in local_addr;
socklen_t local_addrlen = sizeof(struct sockaddr_in);
getsockname(server_sockfd, (struct sockaddr *)&local_addr, &local_addrlen);
printf("local address: %s\n", inet_ntoa(local_addr.sin_addr));
printf("local port: %hu\n", ntohs(local_addr.sin_port));

int buflen;
char buf[BUF_SIZE];
do {
fgets(buf, sizeof(buf), stdin);
buflen = send(server_sockfd, buf, strlen(buf), 0);
if (buflen > 0) {
printf("send: %d\n", buflen);
} else if (buflen == -1) {
fprintf(stderr, "Failed to send bytes to socket %d: %s\n",
server_sockfd, strerror(errno));
exit(1);
}
} while (strncasecmp(buf, "quit", 4) != 0);

close(server_sockfd);
return 0;
}

在一个终端中运行服务器程序,在另一个终端运行客户端程序:

1
2
$ ./tcp_server

1
2
3
4
5
6
$ vi tcp_client.c
$ gcc tcp_client.c -o tcp_client
$ ./tcp_client
local address: 127.0.0.1
local port: 54600

若客户端程序发送 Hello World!,可以看到服务器程序输出 Hello World!

1
2
3
$ ./tcp_server 
Received a message: 13: Hello World!

1
2
3
4
5
6
$ ./tcp_client
local address: 127.0.0.1
local port: 54600
Hello World!
send: 13

附录 C:UDP 服务器程序

该服务器程序在接收到一个 UDP 数据报后,会将其输出并退出。源码如下:

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
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include <stdlib.h> // exit()
#include <stdint.h> // uint8_t
#include <string.h> // memset(), memcpy(), strcpy(), strerror()
#include <unistd.h> // close()
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h> // errno

#define BUF_SIZE 1024

int main()
{
int server_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (server_sockfd == -1) {
perror("Faild to create socket.");
return 1;
}

struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(1080);
server_addr.sin_addr.s_addr = INADDR_ANY;

if (bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in)) == -1) {
perror("Faild to bind socket.");
return 1;
}

struct sockaddr_in client_addr;
socklen_t client_addrlen = sizeof(struct sockaddr_in);

int buflen;
char buf[BUF_SIZE];

buflen = recvfrom(server_sockfd, buf, BUF_SIZE - 1, 0,
(struct sockaddr *)&client_addr, &client_addrlen);
if (buflen > 0) {
buf[buflen] = '\0';
printf("Received a message: %d: %s", buflen, buf);
} else if (buflen == -1) {
fprintf(stderr, "Failed to receive bytes from socket %d: %s\n",
server_sockfd, strerror(errno));
exit(1);
}

close(server_sockfd);
return 0;
}

将以上源码编译生成目标文件并运行:

1
2
3
4
$ vi udp_server.c
$ gcc udp_server.c -o udp_server
$ ./udp_server

在另一终端中,用 nc 命令发送一个 UDP 数据报,终端的输出如下:

1
2
3
4
$ netstat -anup | grep udp_server
udp 0 0 0.0.0.0:1080 0.0.0.0:* 4229/./udp_server
$ nc -u 127.0.0.1 1080
Hello World
1
2
$ ./udp_server
Received a message: 12: Hello World

附录 D:UDP 客户端程序

该客户端程序启动后,向 127.0.0.1:8080 发出一个 UDP 数据报后便退出。源码如下:

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
39
40

#include <stdio.h>
#include <stdlib.h> // exit()
#include <stdint.h> // uint8_t
#include <string.h> // memset(), memcpy(), strcpy(), strerror()
#include <unistd.h> // close()
#include <sys/socket.h>
#include <arpa/inet.h>

#define BUF_SIZE 1024

int main()
{
int client_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (client_sockfd == -1) {
perror("Faild to create socket.");
exit(1);
}

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(1080);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

int buflen;
char buf[BUF_SIZE] = "Hello World!";

buflen = sendto(client_sockfd, buf, strlen(buf), 0,
(struct sockaddr *)&server_addr, sizeof(struct sockaddr_in));
if (buflen > 0) {
printf("Sent a message: %d\n", buflen);
} else if (buflen == -1) {
perror("sendto error");
exit(1);
}

close(client_sockfd);
return 0;
}

在一个终端中运行服务器程序,在另一个终端运行客户端程序:

1
2
$ ./udp_server
Received a message: 12: Hello World!
1
2
3
4
$ vi udp_client.c
$ gcc udp_client.c -o udp_client
$ ./udp_client
Sent a message: 12