下图是基于TCP协议的客户端/服务器程序的一般流程:
图1.1 TCP协议通讯流程
建立链接的过程:
图1.2 建立连接的过程
服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用 socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从 connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。
数据传输的过程:
建立连接后,TCP 协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从 accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务 器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结 果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。
关闭链接的过程:
图1.3 关闭连接的过程
如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了 连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用 shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。
在学习socket API时要注意应用程序和TCP协议层是如何交互的: *应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段 *应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段
先看一下需要用到的函数
1
2
3
4
5
6
7
8
9
10
11
|
NAME
socket - create an endpoint
for
communication
SYNOPSIS
#include <sys/types.h>
/* See NOTES */
#include <sys/socket.h>
int
socket(
int
domain,
int
type,
int
protocol);
DESCRIPTION
socket() creates an endpoint
for
communication and returns a descriptor.
|
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用 read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,family参数指定为AF_INET。对于TCP协 议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的 传输协议。protocol参数的介绍从略,指定为0即可。
1
2
3
4
5
6
7
8
9
|
NAME
bind - bind a name to a socket
SYNOPSIS
#include <sys/types.h>
/* See NOTES */
#include <sys/socket.h>
int
bind(
int
sockfd,
const
struct
sockaddr *addr,
socklen_t addrlen);
|
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。bind()成功返回0,失败返回-1。
bind() 的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。前面讲 过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定 结构体的长度。我们的程序中对myaddr参数是这样初始化的:
1
2
3
4
|
bzero(&servaddr,
sizeof
(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
|
首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡, 每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为 SERV_PORT,我们定义为8000。
1
2
3
4
5
6
7
8
|
NAME
listen - listen
for
connections on a socket
SYNOPSIS
#include <sys/types.h>
/* See NOTES */
#include <sys/socket.h>
int
listen(
int
sockfd,
int
backlog);
|
典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务 器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于 连接待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。
1
2
3
4
5
|
SYNOPSIS
#include <sys/types.h>
/* See NOTES */
#include <sys/socket.h>
int
accept(
int
sockfd,
struct
sockaddr *addr, socklen_t *addrlen);
|
三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。 cliaddr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区cliaddr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者 提供的缓冲区)。如果给cliaddr参数传NULL,表示不关心客户端的地址。
服务器使这样子的:
1
2
3
4
5
6
7
8
|
while
(1) {
cliaddr_len =
sizeof
(cliaddr);
connfd = accept(listenfd,
(
struct
sockaddr *)&cliaddr, &cliaddr_len);
n = read(connfd, buf, MAXLINE);
......
close(connfd);
}
|
整个是一个while死循环,每次循环处理一个客户端连接。由于cliaddr_len是传入传出参数,每次调用accept()之前应该重新赋初值。 accept()的参数listenfd是先前的监听文件描述符,而accept()的返回值是另外一个文件描述符connfd,之后与客户端之间就通过 这个connfd通讯,最后关闭connfd断开连接,而不关闭listenfd,再次回到循环开头listenfd仍然用作accept的参数。 accept()成功返回一个文件描述符,出错返回-1。
TCP网络程序:
server.c的作用是从客户端读字符,然后将每个字符转换为大写并回送给客户端。
/*server.c*/
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
|
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAXLINE 80
#define SERV_PORT 8000
int
main(
void
)
{
struct
sockaddr_in servaddr, cliaddr;
//定义套接字地址
socklen_t cliaddr_len;
int
listenfd, connfd;
char
buf[MAXLINE];
char
str[INET_ADDRSTRLEN];
int
i, n;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr,
sizeof
(servaddr));
//结构体初始化
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
//INADDR_ANY 宏
servaddr.sin_port = htons(SERV_PORT);
//端口号
bind(listenfd, (
struct
sockaddr *)&servaddr,
sizeof
(servaddr));
listen(listenfd, 20);
printf
(
"Accepting connections ...\n"
);
while
(1) {
cliaddr_len =
sizeof
(cliaddr);
connfd = accept(listenfd,
(
struct
sockaddr *)&cliaddr, &cliaddr_len);
//accept()返回时传出客户端的地址和端口号
n = read(connfd, buf, MAXLINE);
printf
(
"received from %s at PORT %d\n"
,
inet_ntop(AF_INET, &cliaddr.sin_addr, str,
sizeof
(str)),
ntohs(cliaddr.sin_port));
for
(i = 0; i < n; i++)
buf[i] =
toupper
(buf[i]);
//用来将字符c转换为大写英文字母
write(connfd, buf, n);
close(connfd);
}
}
|
由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要 调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器 时端口号都不一样,客户端要连接服务器就会遇到麻烦。
1
2
3
4
5
6
7
8
9
|
NAME
connect - initiate a connection on a socket
SYNOPSIS
#include <sys/types.h>
/* See NOTES */
#include <sys/socket.h>
int
connect(
int
sockfd,
const
struct
sockaddr *addr,
socklen_t addrlen);
|
客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。
client.c的作用是从命令行参数中获得一个字符串发给服务器,然后接收服务器返回的字符串并打印。
/*client.c*/
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
|
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAXLINE 80
#define SERV_PORT 8000
int
main(
int
argc,
char
*argv[])
{
struct
sockaddr_in servaddr;
char
buf[MAXLINE];
int
sockfd, n;
char
*str;
if
(argc != 2) {
fputs
(
"usage: ./client message\n"
, stderr);
exit
(1);
}
str = argv[1];
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr,
sizeof
(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET,
"127.0.0.1"
, &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
connect(sockfd, (
struct
sockaddr *)&servaddr,
sizeof
(servaddr));
write(sockfd, str,
strlen
(str));
n = read(sockfd, buf, MAXLINE);
printf
(
"Response from server:\n"
);
write(STDOUT_FILENO, buf, n);
close(sockfd);
return
0;
}
|
打开两个终端,依次运行./server和./client tcp