10.TCP 网络编程流程及例子

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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值