第四章 基于TCP的服务端/客户端(1)
本章讨论的问题是:通过套接字收发数据。
数据传输方式有两种,一种是面向连接的套接字,一种是面向消息的套接字,本章继续讨论面向连接的服务端和客户端的编写。
4.1 理解TCP和UDP
根据数据传输方式不同,基于网络协议的套接字一般分为TCP套接字和UDP套接字。
TCP面向连接,又称基于流的套接字。流(stream)
TCP:transmission control protocol 传输控制协议,对数据传输过程的控制。
TCP/IP协议栈
链路层——IP层——TCP/UDP层——应用层
通过层次化方案解决数据收发问题。
各层可能通过操作系统等软件实现,也可能通过类似NIC的硬件设备实现。
把协议分成多个层次的优点:
- 协议设计更容易
- 分成的主要目的是为了通过标准化操作设计开放式系统
所有的网卡制造商都会按照链路层的协议标准制造网卡;
所有的路由器生产商都会按照IP层标准制造路由器。
链路层
是物理链接领域标准化的结果。
定义LAN、WAN、MAN等网络标准。
IP层
IP层解决数据传输中的路径选择问题,只需按照此路径传输数据即可。
IP本身是面向消息的、不可靠的协议。每次传输数据时会帮我们选择路径,但并不一致。无法对应数据错误。
TCP/UDP层
TCP/UDP层以IP层提供的路径信息为基础完成实际的数据传输,该层又称传输层。
TCP可以保证可靠的数据传输,但它发送数据时以IP层为基础。
IP层只关注1个数据包(数据传输的基本单位)的传输过程,即使传输多个数据包,每个数据包也是由IP层实际传输的,所以传输顺序和传输本身是不可靠的。
但是TCP协议能够做到可靠传输,靠的是确认应答和超时重传等机制。
应用层
上述3层都是套接字通信过程中它自动处理的,选择数据传输路径、数据确认过程都被隐藏到套接字内部,把程序员从这些细节中解放出来。
编写软件时需要根据程序特点决定服务器和客户端之间的数据传输规则(规定),这就是应用层协议。
程序员的主要任务,就是网络编程的主要内容,是设计并实现应用层协议。
4.2 实现基于TCP的服务器端/客户端
TCP服务器端的默认函数调用顺序
socket() 创建套接字
声明并初始化地址信息结构体变量
bind() 分配套接字地址
listen() 等待连接请求状态
accept() 允许连接
read()/write() 数据交换
close() 断开连接
进入等待连接请求状态
当已经调用bind函数给套接字分配了地址,接下来就通过调用listen函数进入等待连接请求状态。
只有调用了listen函数,客户端才能进入可发出连接请求的状态。只有这样,客户端才能调用connect函数(若提前调用将发生错误)。
#include<sys/socket.h>
int listen(int sock, int backlog);
sock 希望进入链接请求状态的套接字文件描述符,传递的描述符套接字参数成为服务器端套接字(监听套接字)
backlog 连接请求等待队列的长度,若为5,则队列长度为5,表示最多使5个连接请求进入队列
成功时返回0,失败时返回-1
服务器端套接字像个门卫,调用listen函数可以生成这种门卫,把想连接到服务器端的客户端的请求们,迎接到服务器端的连接请求等候室内。等候室——>连接请求等待队列,等候室里他们的状态——>等待连接请求状态。
受理客户端连接请求
调用listen后,应依序进行受理。受理请求,意味着进入可接受数据的状态。
accept函数会自动创建一个套接字,并连接到发起请求的客户端。
#include<sys/socket.h>
int accept(int sock, struct sockaddr * addr, socklen_t * addrlen);
sock 服务器套接字的文件描述符
addr 保存发起连接请求的客户端地址信息的变量地址值,调用函数后,向传递来的地址变量参数填充客户端地址信息
addrlen 存有结构体addr的长度的变量地址。函数调用完成后,所谓变量中的内容是客户端地址的长度
accept函数受理“连接请求等待队列中”那些“待处理的客户端连接请求”。
函数调用成功时,accept函数内部将产生用于数据I/O的套接字,并返回其文件描述符。
从等待队列中取出1个连接请求,创建套接字并完成连接请求。服务器端单独创建的套接字与客户端建立连接后进行数据交换。
TCP客户端的默认函数调用顺序
创建套接字、请求连接,是客户端的全部内容。
socket() 创建套接字
connect() 请求连接
read()/write() 交换数据
close() 断开连接
发起连接请求:
#include<sys/socket.h>
int connect(int sock, struct sockaddr* servaddr, socklen_t addrlen);
sock 客户端套接字文件描述符
servaddr 保存目标服务器端地址信息的变量地址值
addrlen 以字节为单位传递已传递给第二个结构体参数servaddr的地址变量长度
成功时返回0,失败时返回-1
客户端调用connect函数之后,发生以下情况之一才会返回,否则阻塞:
- 服务器端接收连接请求(并不意味这服务器端调用accept函数,其实是服务器端把连接请求信息记录到等待队列。故,connect函数返回后并不立即进行数据交换。)
- 发生断网等异常情况而中断连接请求
客户端套接字地址信息分配时机3问:
- 何时分配?调用connect函数时
- 何地分配?操作系统,更准确来说是在内核中
- 如何分配?IP用计算机(主机)的IP,端口随机
基于TCP的服务器端/客户端函数调用关系
4.3 实现迭代服务器端/客户端
echo_server.c :
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char * message);
int main(int argc,char* argv[]){
int serv_sock,clnt_sock;
char message[BUF_SIZE];
int str_len, i;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_sz;
if(argc != 2){
printf("Usage:%s <port>\n",argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1){
error_handling("socket() error");
}
memset(&serv_adr,0,sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock,(struct sockaddr*)& serv_adr,sizeof(serv_adr)) == -1){
error_handling("bind() error");
}
if(listen(serv_sock, 5) == -1){
error_handling("listen() error");
}
clnt_adr_sz = sizeof(clnt_adr);
for(i=0;i<5;++i){
clnt_sock = accept(serv_sock,(struct sockaddr*)& clnt_adr, &clnt_adr_sz);
if(clnt_sock == -1)
error_handling("accept() error");
else
printf("Connected client %d \n",i+1);
while((str_len = read(clnt_sock,message,BUF_SIZE)) != 0){
write(clnt_sock, message, str_len);
}
close(clnt_sock);
}
close(serv_sock);
return 0;
}
void error_handling(char * message){
fputs(message, stderr);
fputc('\n',stderr);
exit(1);
}
echo_client.c :
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char * message);
int main(int argc, char* argv[]){
int sock;
char message[BUF_SIZE];
int str_len;
struct sockaddr_in serv_adr;
if(argc != 3){
printf("Usage:%s <port> \n",argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1){
error_handling("socket() error");
}
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
if(connect(sock,(struct sockaddr*)& serv_adr, sizeof(serv_adr)) == -1){
error_handling("connect() error");
}else{
puts("Connected.....");
}
while(1){
fputs("Input message(Q to quit):",stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n")){
break;
}
write(sock, message, strlen(message));
str_len = read(sock, message, BUF_SIZE-1);
message[str_len] = 0;
printf("Message from server:%s",message);
}
close(sock);
return 0;
}
void error_handling(char * message){
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
[root@VM_0_10_centos echotest] gcc echo_server.c -o eserver
[root@VM_0_10_centos echotest] gcc echo_client.c -o eclient
[root@VM_0_10_centos echotest] ll
total 36
-rw-r--r-- 1 root root 1226 Jun 29 21:20 echo_client.c
-rw-r--r-- 1 root root 1369 Jun 29 20:47 echo_server.c
-rwxr-xr-x 1 root root 13472 Jun 29 21:21 eclient
-rwxr-xr-x 1 root root 9160 Jun 29 20:47 eserver
[root@VM_0_10_centos echotest] ./eclient 127.0.0.1 9190
Connected client 1
Connected.....
Input message(Q to quit):good night
Message from server:good night
Input message(Q to quit):hello world
Message from server:hello world
Input message(Q to quit):q
[root@VM_0_10_centos echotest]#
这样就实现了回声功能。
但仍有缺陷:
- 每次调用write函数都会传递1个字符串,但TCP不存在数据边界,多次调用write函数传递的字符串有可能一次性传递到服务器端,这样客户端有可能从服务器端收到多个字符串,这就是bug1;
- 如果数据过大,操作系统可能会把数据分成多个数据包发送到客户端,在此过程中,客户端可能在尚未收完全部数据包时就调用了read函数,这样显示的内容就不全了,这就是bug2。
4.4 基于Windows的实现
需做4点改变:
- 通过
WSAStartup
、WSACleanup
函数初始化并清除套接字相关库 - 把数据类型和变量名切换为Windows风格
- 数据传输中用
recv
、send
函数,而非read
、write
函数 - 关闭套接字时用
closesocket
函数而非close
函数
4.5 习题
(1)请说明TCP/IP的4层协议栈,并说明TCP和UDP套接字经过的层级结构差异。
链路层—>IP层—>TCP层—>应用层
链路层—>IP层—>UDP层—>应用层
(2)请说出TCP/IP协议栈中链路层和IP层的作用,并给出两者关系。
链路层是LAN、WAN、MAN等网络标准相关的协议栈,是定义物理性质标准的层级。相反,IP层是定义网络传输数据标准的层级。即IP层负责以链路层为基础的数据传输。
(3)为何需要把TCP/IP协议栈分成4层(或7层)?结合开放式系统回答。
将复杂的TCP/IP
协议分层化的话,就可以将分层的层级标准发展成开放系统。实际上,TCP/IP
是开放系统,各层级都被初始化,并以该标准为依据组成了互联网。因此,按照不同层级标准,硬件和软件可以相互替代,这种标准化是TCP/IP蓬勃发张的依据。
(4)客户端调用connect函数向服务器端发送连接请求。服务器端调用哪个函数后,客户端可以调用connect函数?
服务器端调用了listen
函数后,客户端才可以调用connect
函数。
(5)什么时候创建连接请求等待队列?它有何作用?与accept有什么关系。
listen
函数的调用创建了请求等待队列。它是存储客户端连接请求信息的空间。accept
函数调用后,将从本地存储的连接请求信息取出,与客户端建立连接。
(6)客户端中为何不需要调用bind函数分配地址?如果不调用bind函数,那何时、如何向套接字分配IP地址和端口号?
客户端是请求连接的程序,不是一个接收连接的程序。所以,指导服务器的地址信息是更重要的因素,没有必要通过bind函数明确地分配地址信息。但是,要想和服务器通信,必须将自己的地址信息分配到套接字上,因此,在connect函数调用时,自动把IP地址和端口号输入到套接字上
(7)把第1章的hello_server.c和hello_server_win.c改成迭代服务器端,并利用客户端测试更改是否准确。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;
char message[]="Hello World!";
if(argc!=2){
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_addr.sin_port=htons(atoi(argv[1]));
if( bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1 )
error_handling("bind() error");
if( listen(serv_sock, 5)==-1 )
error_handling("listen() error");
clnt_addr_size=sizeof(clnt_addr);
while(1)
{
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size);
if(clnt_sock==-1)
break;
write(clnt_sock, message, sizeof(message));
close(clnt_sock);
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}