TCP 网络编程是目前比较通用的方式,例如 HTTP 协议、FTP 协议等很多广泛应用的
协议均基于 TCP 协议。TCP 编程主要为 C/S 模式,即客户端(C)、服务器(S)模式,这
两种模式之间的程序设计流程存在很大的差别
1. TCP 网络编程架构
TCP 网络编程有两种模式,一种是服务器模式,另一种是客户端模式。服务器模式创
建一个服务程序,等待客户端用户的连接,接收到用户的连接请求后,根据用户的请求进
行处理;客户端模式则根据目的服务器的地址和端口进行连接,向服务器发送请求并对服
务器的响应进行数据处理。
如图 7.4 所示为 TCP 连接的服务器模式的程序设计流程。流程主要分为套接字初始化
(socket()函数),套接字与端口的绑定(bind()函数),设置服务器的侦听连接(listen()函数),
接受客户端连接(accept()函数),接收和发送数据(read()函数、write()函数)并进行数据
处理及处理完毕的套接字关闭(close()函数)。
流程
-
套接字初始化过程中,根据用户对套接字的需求来确定套接字的选项。这个过程
中的函数为 socket(),它按照用户定义的网络类型、协议类型和具体的协议标号等
参数来定义。系统根据用户的需求生成一个套接字文件描述符供用户使用。
-
套接字与端口的绑定过程中,将套接字与一个地址结构进行绑定。绑定之后,在
进行网络程序设计的时候,套接字所代表的 IP 地址和端口地址,以及协议类型等
参数按照绑定值进行操作。
-
由于一个服务器需要满足多个客户端的连接请求,而服务器在某个时间仅能处理
有限个数的客户端连接请求,所以服务器需要设置服务端排队队列的长度。服务
器侦听连接会设置这个参数,限制客户端中等待服务器处理连接请求的队列长度。
-
在客户端发送连接请求之后,服务器需要接收客户端的连接,然后才能进行其他
的处理。
-
在服务器接收客户端请求之后,可以从套接字文件描述符中读取数据或者向文件
描述符发送数据。接收数据后服务器按照定义的规则对数据进行处理,并将结果
发送给客户端。
-
当服务器处理完数据,要结束与客户端的通信过程的时候,需要关闭套接字连接
客户端的程序设计模式
主要分为套接字初始化(socket()函数),连接服务器(connect()
函数),读写网络数据(read()函数、write()函数)并进行数据处理和最后的套接字关闭(close()
函数)过程。
客户端连接服务器的处理过程中,客户端根据用户设置的服务器地址、端口等参数与
特定的服务器程序进行通信。
客户端与服务器的交互过程
客户端与服务器在连接、读写数据、关闭过程中有交互过程。
客户端的连接过程,对服务器端是接收过程,在这个过程中客户端与服务器进行
三次握手,建立 TCP 连接。建立 TCP 连接之后,客户端与服务器之间可以进行数
据的交互。
客户端与服务器之间的数据交互是相对的过程,客户端的读数据过程对应了服务
器端的写数据过程,客户端的写数据过程对应服务器的读数据过程。
在服务器和客户端之间的数据交互完毕之后,关闭套接字连接。
2.通信实现(举例服务端)
头文件:
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
1.sruct sockadd_in my_add; /*以太网套接字地址结构*/
2.sockfd = socket(AF_INET, SOCK_STREAM, 0); /*初始化 socket*/
判断返回值
socket():
原型: int socket(int domain, int type, int protocol);
返回值: 文件描述符fd(自定义)
作用: 创建网络插口, 获得文件描述符
3. my_addr.sin_family = AF_INET; /*地址结构的协议族*/
my_addr.sin_port = htons(MYPORT); /*地址结构的端口地址,网络字节序*/
my_addr.sin_addr.s_addr = inet_addr("192.168.1.150");/*IP,将字符串的 IP 地址转化为网络字节序*/
4. bzero(&(my_addr.sin_zero), 8); /*将 my_addr.sin_zero 置为 0*/
5. bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr);绑定及判断
bind():
原型: int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
返回值: 0 时表示绑定成功,–1 表示绑定失败
作用: 绑定一个地址端口, 在建立套接字文件描述符成功后,需要对套接字进行地址和端口的绑定,才 能进行数据的接收和发送操作。
6. listen(sockfd, 5)/*进行侦听队列长度的绑定*/
listen():
原型: int listen(int sockfd, int backlog);
作用: 初始化服务器可连接队列, 服务器处理客户端连接请求的时候是顺序处理的,同一时间仅能处理 一个客户端连接, 当多个客户端的连接请求同时到来的时候,服务器并不是同时处理,而是将 不能处理的客户端连接请求放到等待队列中
7. addr_length = sizeof(struct sockaddr_in); /*地址长度*/
8. client_fd = accept(sockfd, & client_addr, & addr_length);/*等待客户端连接,地址在 client_addr 中*/
accept():
原型: int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
返回值:新的文件描述符,新的描述符用于读和写操作,旧的描述符一直在监听
9. int size ;
char data[1024];
size = write(client_fd, data, 1024);
10. size = read(client_fd, data, 1024);
11. close(sockfd);
12. close(client_fd);
客户端
有一个connect函数
01 #define DEST_IP "132.241.5.10" /*服务器的 IP 地址*/
02 #define DEST_PORT 23 /*服务器端口*/
03 int main(int argc, char *argv[])
04 {
05 int ret = 0;
06 int sockfd; /*sockfd 为连接的 socket*/
07 struct sockaddr_in server; /*服务器地址的信息*/
08 sockfd = socket(AF_INET, SOCK_STREAM, 0);
/*初始化一个IPv4族的流式连接*/
09 if (sockfd == -1) { /*检查是否正常初始化 socket*/
10 perror("socket");
11 exit(EXIT_FAILURE);
12 }
13 server.sin_family = AF_INET; /*协议族为 IPv4,主机字节序*/
14 server.sin_port = htons(DEST_PORT); /*端口,短整型,网络字节序*/
15 server.sin_addr.s_addr = htonl(DEST_IP); /*服务器的 IP 地址*/
16 bzero(&(server.sin_zero), 8); /*保留字段置 0*/
17
18 ret = connect(sockfd, (struct sockaddr *)& server, sizeof(struct
sockaddr)); /*连接服务器*/
19 ... /*接收或者发送数据*/
20 close(sockfd);
21 }
3. 关闭套接字函数
关闭 socket 连接可以使用 close()函数实现,函数的作用是关闭已经打开的 socket 连接,
内核会释放相关的资源,关闭套接字之后就不能再使用这个套接字文件描述符进行读写操
作了。函数原型在第 3 章中已经介绍过。
函数 shutdown()可以使用更多方式来关闭连接,允许单方向切断通信或者切断双方的
通信。函数原型如下,第一个参数 s 是切断通信的套接口文件描述符,第二个参数 how 表
示切断的方式。
#include <sys/socket.h>
int shutdown(int s, int how);
函数 shutdown()用于关闭双向连接的一部分,具体的关闭行为方式通过参数的 how 设
置来实现。可以为如下值。
SHUT_RD:值为 0,表示切断读,之后不能使用此文件描述符进行读操作。
SHUT_WR:值为 1,表示切断写,之后不能使用此文件描述符进行写操作。
SHUT_RDWR:值为 2,表示切断读写,之后不能使用此文件描述符进行读写操作,
与 close()函数功能相同。
函数 shutdown()如果调用成功则返回 0,如果失败则返回–1,通过 errno 可以获得错误。
例子功能描述:
例子程序分为服务器端和客户端,客户端连接服务器后从标准输入读取输入的字符
串,发送给服务器;服务器接收到字符串后,发送接收到的总字符串个数给客户端;客户
端将接收到的服务器的信息打印到标准输出。程序框架如图 7.11 所示
程序按照网络流程建立套接字、初始化绑定网络地址、将套接字与
网络地址绑定、设置侦听队列长度、接收客户端连接、收发数据、关闭套接字进行编写。
1.初始化工作
包含需要的头文件、定义侦听端口及侦听队列的长度。
01 #include <stdio.h>
02 #include <stdlib.h>
03 #include <strings.h>
04 #include <sys/types.h>
05 #include <sys/socket.h>
06 #include <arpa/inet.h>
07 #include <unistd.h>
08 #define PORT 8888 /*侦听端口地址*/
09 #define BACKLOG 2 /*侦听队列长度*/
10 int main(int argc, char *argv[])
11 {
12 int ss,sc; /*ss 为服务器的 socket 描述符,sc 为客户端的 socket 描述符*/
13 struct sockaddr_in server_addr; /*服务器地址结构*/
14 struct sockaddr_in client_addr; /*客户端地址结构*/
15 int err; /*返回值*/
16 pid_t pid; /*分叉的进行 ID*/
2.建立套接字
建立一个 AF_INET 域的流式套接字。
17 /*建立一个流式套接字*/
18 ss = socket(AF_INET, SOCK_STREAM, 0);
19 if(ss < 0){ /*出错*/
20 printf("socket error\n");
21 return -1;
22 }
3.设置服务器地址
在给地址和端口进行赋值的时候使用了 htonl()函数和 htohs()函数,这是两个网络字节
序和主机字节序进行转换的函数。
23 /*设置服务器地址*/
24 bzero(&server_addr, sizeof(server_addr)); /*清零*/
25 server_addr.sin_family = AF_INET; /*协议族*/
26 server_addr.sin_addr.s_addr = htonl(INADDR_ANY); /*本地地址*/
27 server_addr.sin_port = htons(PORT); /*服务器端口*/
4.绑定地址到套接字描述符
将上述设置好的网络地址结构与套接字进行绑定。
28 /*绑定地址结构到套接字描述符*/
29 err = bind(ss, (struct sockaddr*)&server_addr, sizeof(server_addr));
30 if(err < 0){ /*出错*/
31 printf("bind error\n");
32 return -1;
33 }
5.设置侦听队列
将套接字的侦听队列长度设置为 2,可以同时处理两个客户端的连接请求。
34 /*设置侦听*/
35 err = listen(ss, BACKLOG);
36 if(err < 0){ /*出错*/
37 printf("listen error\n");
38 return -1;
39 }
6.主循环过程
在主循环过程中为了方便处理,每个客户端的连接请求服务器会分叉一个进程进行处
理。函数 fork()出来的进程继承了父进程的属性,例如套接字描述符,在子进程和父进程
中都有一套。
为了防止误操作,在父进程中关闭了客户端的套接字描述符,在子进程中关闭了父进
程中的侦听套接字描述符。一个进程中的套接字文件描述符的关闭,不会造成套接字的真
正关闭,因为仍然有一个进程在使用这些套接字描述符,只有所有的进程都关闭了这些描
述符,Linux 内核才释放它们。在子进程中,处理过程通过调用函数 process_conn_server()
来完成。
40 /*主循环过程*/
41 for(;;) {
42 socklen_t addrlen = sizeof(struct sockaddr);
43
44 sc = accept(ss, (struct sockaddr*)&client_addr, &addrlen);
45 /*接收客户端连接*/
46 if(sc < 0){ /*出错*/
47 continue; /*结束本次循环*/
48 }
49
50 /*建立一个新的进程处理到来的连接*/
51 pid = fork(); /*分叉进程*/
52 if( pid == 0 ){ /*子进程中*/
53 close(ss); /*在子进程中关闭服务器的侦听*/
54 }else{
55 close(sc); /*在父进程中关闭客户端的连接*/
56 }
57 }
58 }
7.3.3 服务器读取和显示字符串
服务器端对客户端连接的处理过程如下,先读取从客户端发送来的数据,然后将接收
到的数据个数发送给客户端。
01 /*服务器对客户端的处理*/
02 void process_conn_server(int s)
03 {
04 ssize_t size = 0;
05 char buffer[1024]; /*数据的缓冲区*/
06
07 for(;;){ /*循环处理过程*/
08 size = read(s, buffer, 1024);
/*从套接字中读取数据放到缓冲区 buffer 中*/
09 if(size == 0){ /*没有数据*/
10 return;
11 }
12
13 /*构建响应字符,为接收到客户端字节的数量*/
14 sprintf(buffer, "%d bytes altogether\n", size);
15 write(s, buffer, strlen(buffer)+1);/*发给客户端*/
16 }
17 }
7.3.4 客户端的网络程序
客户端的程序十分简单,建立一个流式套接字后,将服务器的地址和端口绑定到套接
字描述符上;然后连接服务器,进程处理;最后关闭连接。
01 #include <stdio.h>
02 #include <stdlib.h>
03 #include <string.h>
04 #include <sys/types.h>
05 #include <sys/socket.h>
06 #include <unistd.h>
07 #include <arpa/inet.h>
08 #define PORT 8888 /*侦听端口地址*/
09 int main(int argc, char *argv[])
10 {
11 int s; /*s 为 socket 描述符*/
12 struct sockaddr_in server_addr; /*服务器地址结构*/
13
14 s = socket(AF_INET, SOCK_STREAM, 0); /*建立一个流式套接字 */
15 if(s < 0){ /*出错*/
16 printf("socket error\n");
17 return -1;
18 }
19
20 /*设置服务器地址*/
21 bzero(&server_addr, sizeof(server_addr)); /*清零*/
22 server_addr.sin_family = AF_INET; /*协议族*/
23 server_addr.sin_addr.s_addr = htonl(INADDR_ANY); /*本地地址*/
24 server_addr.sin_port = htons(PORT); /*服务器端口*/
25
第 2 篇 Linux 用户层网络编程
·206·
26 /*将用户输入的字符串类型的 IP 地址转为整型*/
27 inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
28 /*连接服务器*/
29 connect(s, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
30 process_conn_client(s); /*客户端处理过程*/
31 close(s); /*关闭连接*/
32 return 0;
33 }
7.3.5 客户端读取和显示字符串
客户端从标准输入读取数据到缓冲区 buffer 中,发送到服务器端。然后从服务器端读
取服务器的响应,将数据发送到标准输出。
01 /*客户端的处理过程*/
02 void process_conn_client(int s)
03 {
04 ssize_t size = 0;
05 char buffer[1024]; /*数据的缓冲区*/
06
07 for(;;){ /*循环处理过程*/
08 /*从标准输入中读取数据放到缓冲区 buffer 中*/
09 size = read(0, buffer, 1024);
10 if(size > 0){ /*读到数据*/
11 write(s, buffer, size); /*发送给服务器*/
12 size = read(s, buffer, 1024); /*从服务器读取数据*/
13 write(1, buffer, size); /*写到标准输出*/
14 }
15 }
16 }
注意:使用 read()和 write()函数时,文件描述符 0 表示标准输入,1 表示标准输出,可
以直接对这些文件描述符进行操作,例如读和写。
7.3.6 编译运行程序
服务器的网络程序保存为文件 tcp_server.c、客户端的网络程序保存为 tcp_client.c、客
户端和服务器的字符串处理保存为文件 tcp_proccess.c,建立如下的 Makefile 文件:
all:client server #all 规则,它依赖于 client 和 server 规则
client:tcp_process.o tcp_client.o #client 规则,生成客户端可执行程序
gcc -o client tcp_process.o tcp_client.o
server:tcp_process.o tcp_server.o #server 规则,生成服务器端可执行程序
gcc -o server tcp_process.o tcp_server.o
tcp_process.o: #tcp_process.o 规则,生成 tcp_process.o
gcc -c tcp_process.c -o tcp_process.o
clean: #清理规则,删除 client、server 和中间文件
rm -f client server *.o
将 tcp_process.c、tcp_client.c、tcp_server.c 分别先编译成 tcp_process.o、tcp_client.o 和
tcp_server.o,然后把 tcp_process.o 和 tcp_client.o 编译成 tcp_client 可执行文件,将
tcp_process.o 和 tcp_server.o 编译成 tcp_server 可执行文件。
$ make
gcc -c tcp_process.c -o tcp_process.o
cc -c -o tcp_client.o tcp_client.c
gcc -o client tcp_process.o tcp_client.o
cc -c -o tcp_server.o tcp_server.c
gcc -o server tcp_process.o tcp_server.o
先运行服务器端可执行程序 server,这个程序会在 8888 端口侦听,等待客户端的连接
请求。
Debian#./server
在另一个窗口运行客户端,并输入 hello 和 nihao 字符串。服务器端将客户端发送的数
据进行计算并返回给客户端,结果如下:
$ ./client 127.0.0.1
hello
6 bytes altogether
nihao
6 bytes altogether
使用 netstat 命令查询网络连接情况,8888 是服务器的端口,55143 的端口,服务器和
客户端通过这两个端口建立了连接。
$netstat
tcp 0 0 localhost:55143 localhost:8888 ESTABLISHED