1.TCP/IP简介
TCP/IP协议(Transmission Control Protocol / Internet Protocol)叫做传输控制/网际协议,又叫做网络通信协议。它包含了上百个功能的协议,如ICMP、RIP、TELNET、FTP、SMTP、ARP、TFTP等,这些协议一起被称为TCP/IP协议。下表为协议族中一些常用的协议的英文名和含义。
协议名称 | 说明 |
TCP | 传输控制协议 |
IP | 网际协议 |
UDP | 用户数据报协议 |
ICMP | 互联网控制协议 |
SMTP | 简单邮件传输协议 |
SNMP | 简单网络管理协议 |
FTP | 文件传输协议 |
ARP | 地址解析协议 |
阶层 | 说明 |
应用层 | 包括网络应用程序和网络进程,是与用户交互地界面,它为用户提供所需的各种服务,包括文件传输、远程登录和电子邮件等 |
传输层 | 负责相邻计算机之间的通信 |
网络层 | 用来处理计算机之间的通信问题,它接收传输层请求,传输某个具有目的地址信息的分组 |
网络接口层 | 这是TCP/IP协议的最底层,负责接收IP数据报和把数据报通过选定的网络发送出去 |
2.网络编程基础概念介绍
端口:指计算机中为了标识在计算机中访问网络的不同程序而设的编号。每一个程序在访问网络时都会分配一个标识符,程序在访问网络或接受访问时,会用这个标识符表示这一网络数据属于这个程序。这里的端口是指不同程序的逻辑编号而不是网卡接线的端口。端口号是一个16位的无符号整数,对应的十进制取值范围是0~65535.不同编号范围的端口有不同的作用。低于256的端口是系统保留端口号,主要用于系统进程通信。而不在这一范围的端口号是自由端口号,在编程时可以调用这些端口号。
socket端口:socket是网络编程的一种接口,它是一种特殊的I/O。在TCP/IP协议中,“IP地址+TCP或UDP端口号”可以唯一的标识网络通信中的一个进程。可以简单的认为:“IP地址+端口号”就称为socket。在TCP协议中,建立连接的两个进程各自有一个socket来标识,这两个socket组成的socket对就唯一的标识一个连接。用socket函数建立一个socket连接,此函数返回一个整形的socket描述符,随后进行数据传输。通常socket分为三种类型:流式socket、数据报socket和原始socket。
socket套接口:区分不同应用程序进程间的网络通信和连接,主要使用三个参数:通信的目的IP地址、使用的传输层协议(TCP或UDP)和使用的端口号。在编程时,就是使用这三个参数来构成一个套接字。这个套接字相当于一个接口,可以进行不同计算机程序的信息传输。因此,一个IP地址,一个通信端口,就能确定一个通信程序的位置。为此,开发人员专门设计了一个套接结构,就是把网络程序中所用到的网络地址和端口信息放在一个结构体中。一般,套接口地址都以“sockaddr”开头。socket根据所使用的协议的不同,可分为TCP套接口和UDP套接口。
socket套接口的数据结构: 主要有两种数据结构:sockaddr和sockaddr_in,如下图所示:
sockaddr用来保存一个套接字,定义方法如下所示:
struct sockaddr
{
unsigned short int sa_family;
char sa_data[14];
};
sa_family:指定通信的地址类型。如果是TCP/IP通信,则该值为AF_INET。
sa_data:最多使用14个字符长度,用来保存IP地址和端口信息。
sockaddr_in的功能与sockaddr相同,也是用来保存IP地址和端口信息。不同的是将IP地址与读啊口号分开为不同的成员。这个结构体的定义方法如下所示。
struct sockaddr_in
{
unsigned short int sin_family;
uint16_t sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
sin_family:与sockaddr结构体中的sa_family相同。
sin_port:套接字使用的端口号。
sin_addr:需要访问的IP地址。
sin_zero:未使用的字段,填充为0.
3.TCP编程
网络上绝大多数的通信服务采用服务器机制,TCP提供的是一种可靠的、面向连接的服务。基于TCP网络编程的函数及功能如下表所示
函数名 | 功能 |
socket | 用于建立一个socket连接 |
bind | 将socket与本机上的一个端口绑定,随后就可以在该端口监听服务请求 |
connect | 面向连接的客户程序使用connect函数来配置socket,并与远程服务器建立一个TCP连接 |
listen | listen函数使socket处于被动的监听模式,并为该socket建立一个输入数据队列,将到达的服务请求保存在此队列中,直到程序处理它们 |
accept | accept函数让服务器接收客户的连接请求 |
close | 停止在该socket上的任何数据操作 |
send | 数据发送函数 |
recv | 数据接收函数 |
分别编写服务器端、客户端程序,服务器通过socket连接后,在服务器上显示客户端的IP地址或域名,从客户端读字符,然后将每个字符转换为大写并回送给客户端。客户端发送字符串“连接上了”,客户端把接收到的字符串显示在屏幕上。
服务器端程序代码如下:
#include<stdio.h>
#include<netdb.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/wait.h>
#define SERVPORT 3333
#define BACKLOG 10
int main()
{
int sockfd,client_fd;
struct sockaddr_in my_addr;
struct sockaddr_in remote_addr;
int sin_size;
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
perror("socket create fail");
exit(1);
}
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(SERVPORT);
my_addr.sin_addr.s_addr = INADDR_ANY;
bzero(&(my_addr.sin_zero),8);
if(bind(sockfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr)) == -1)
{
perror("bind err!");
exit(1);
}
if(listen(sockfd,BACKLOG) == -1)
{
perror("listen err!");
exit(1);
}
while(1)
{
sin_size = sizeof(struct sockaddr_in);
if((client_fd = accept(sockfd,(struct sockaddr *)&remote_addr,&sin_size)) == -1)
{
perror("accept error");
continue;
}
printf("receive a connection from :%d\n",inet_ntoa(remote_addr.sin_addr));
if(!fork())
{
if(send(client_fd,"has been connected\n",26,0) == -1)
perror("send err!");
close(client_fd);
exit(0);
}
close(client_fd);
}
}
与服务器端同位于socket1文件夹的makefile文件如下:
CC = gcc
AR = $(CC)ar
EXEC = skts
OBJS = skts.o
all: $(EXEC)
$(EXEC): $(OBJS)
$(CC) -o $@ $(OBJS) -lm
clean:
-rm-f $(EXEC) *.elf *.gdb *.o
在文件夹socket2中创建客户端源程序sktc.c和makefile文件如下所示:
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<netdb.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/socket.h>
#define SERVPORT 3333
#define MAXDATASIZE 100
int main(int argc,char * argv[])
{
int sockfd,recvbytes;
char buf[MAXDATASIZE];
struct hostent * host;
struct sockaddr_in serv_addr;
if(argc < 2)
{
fprintf(stderr,"Pleae enter the server's hostname!!\n");
exit(1);
}
if((host = gethostbyname(argv[1])) == NULL)
{
perror("gethostyname error!");
exit(1);
}
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
perror("socket create error!");
exit(1);
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERVPORT);
serv_addr.sin_addr = * ((struct in_addr *)host->h_addr);
bzero(&(serv_addr.sin_zero),8);
if(connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr)) == -1)
{
perror("connect error!");
exit(1);
}
if((recvbytes = recv(sockfd,buf,MAXDATASIZE,0)) == -1)
{
perror("connect error!");
exit(1);
}
buf[recvbytes] = '\0';
printf("recv: %s",buf);
close(sockfd);
}
CC = gcc
AR = $(CC)ar
EXEC = sktc
OBJS = sktc.o
all: $(EXEC)
$(EXEC): $(OBJS)
$(CC) -o $@ $(OBJS) -lm
clean:
-rm-f $(EXEC) *.elf *.gdb *.o
分别执行make命令:
运行结果如下:
分别编写服务器端和客户端程序。服务器端程序的作用是从客户端读字符,然后将每个字符转换为大写并回送给客户端。客户端程序的作用是从命令行参数中获得一个字符串发送给服务器,然后接收返回的字符串并打印。
服务器端源程序skts.c如下:
#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 = htons(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);
n = read(connfd,buf,MAXLINE);
printf("received from %d 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]);
write(connfd,buf,n);
close(connfd);
}
}
服务器端makefile文件:
CC = gcc
AR = $(CC)ar
EXEC = skts
OBJS = skts.o
all: $(EXEC)
$(EXEC): $(OBJS)
$(CC) -o $@ $(OBJS) -lm
clean:
-rm-f $(EXEC) *.elf *.gdb *.o
客户端源程序sktc.c如下:
#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,"10.0.2.15",&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;
}
客户端makefile文件如下:
CC = gcc
AR = $(CC)ar
EXEC = sktc
OBJS = sktc.o
all: $(EXEC)
$(EXEC): $(OBJS)
$(CC) -o $@ $(OBJS) -lm
clean:
-rm-f $(EXEC) *.elf *.gdb *.o
分别执行make命令:
程序的运行结果如下:
4.UDP编程
由于UDP不需要维护连接,程序逻辑简单很多,但是UDP协议是不可靠的,实际上有很多保证通讯可靠性的机制需要在应用层实现。下面介绍的是一个基于UDP协议的编程。其最主要的特点是在客户端不需要用函数bind把本地IP与端口号进行绑定也能进行通信。常用的基于UDP网络编程的函数及功能如下表所示:
函数名 | 功能 |
bind | 将socket与本机上的一个端口绑定,随后就可以在该端口监听服务请求 |
close | 停止在该socket上的任何数据操作 |
sendto | 数据发送函数 |
recvfrom | 数据接收函数 |
服务器端程序代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<netdb.h>
#include<errno.h>
#include<sys/types.h>
int port = 8888;
int main()
{
int sockfd;
int len;
int z;
char buf[256];
struct sockaddr_in adr_inet;
struct sockaddr_in adr_clnt;
printf("等待客户端...\n");
adr_inet.sin_family = AF_INET;
adr_inet.sin_port = htons(port);
adr_inet.sin_addr.s_addr = htonl(INADDR_ANY);
bzero(&(adr_inet.sin_zero),8);
len = sizeof(adr_clnt);
sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1)
{
perror("socket error");
exit(1);
}
z = bind(sockfd,(struct sockaddr *)&adr_inet,sizeof(adr_inet));
if(z == -1)
{
perror("bind error");
exit(1);
}
while(1)
{
z = recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&adr_clnt,&len);
if(z<0)
{
perror("recvfrom error");
exit(1);
}
buf[z] = '\0';
printf("接收:%s",buf);
if(strncmp(buf,"stop",4) == 0)
{
printf("结束...\n");
break;
}
}
close(sockfd);
exit(0);
}
客户端程序代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<netdb.h>
#include<errno.h>
#include<sys/types.h>
int port = 8888;
int main()
{
int sockfd;
int i = 0;
int z;
char buf[80],str1[80];
struct sockaddr_in adr_srvr;
FILE * fp;
printf("打开文件......\n");
fp = fopen("wmj","r");
if(fp == NULL)
{
perror("打开文件失败");;
exit(1);
}
printf("连接服务器...\n");
adr_srvr.sin_family = AF_INET;
adr_srvr.sin_port = htons(port);
adr_srvr.sin_addr.s_addr = htonl(INADDR_ANY);
bzero(&(adr_srvr.sin_zero),8);
sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1)
{
perror("socket error");
exit(1);
}
printf("发送文件...\n");
for(i=0;i<3;i++)
{
fgets(str1,80,fp);
printf("%d:%s",i,str1);
sprintf(buf,"%d:%s",i,str1);
z = sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&adr_srvr,sizeof(adr_srvr));
if(z<0)
{
perror("sendto error");
exit(1);
}
}
printf("发送......\n");
sprintf(buf,"stop\n");
z = sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&adr_srvr,sizeof(adr_srvr));
if(z<0)
{
perror("sendto error");
exit(1);
}
fclose(fp);
close(sockfd);
exit(0);
}
文件wmj的内容:
hello world
hello linux
hello linux world
程序的运行结果如下:
5.网络高级编程
前面介绍了socket通信中常用的函数,利用这些函数能满足基本的socket通信需要;同时也介绍了这些函数用来编写面向连接的网络通信程序和面向无连接的网络程序设计。但在socket应用中,还有一个很重要的问题,那就是如何处理阻塞,解决I/O多路利用问题。在数据通信中,当服务器运行函数accept()时,假设没有客户机连接请求到来,那么服务器就会一直停在accept()语句上,等待客户机连接请求的到来,出现这样的情况就称为阻塞。
下列程序应用函数select处理阻塞问题,程序运行时,如果在设置的10.5秒内没有输入,程序就会显示“超时”;如果在10.5秒内,有输入并回车,则给出显示“输入了”。
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
#define STDIN 0
int main()
{
struct timeval tv;
fd_set readfds;
tv.tv_sec = 10;
tv.tv_usec = 500000;
FD_ZERO(&readfds);
FD_SET(STDIN,&readfds);
select(STDIN + 1,&readfds,NULL,NULL,&tv);
if(FD_ISSET(STDIN,&readfds))
printf("输入了\n");
else
printf("超时\n");
}
程序运行结果如下:
下面编写一个聊天程序
服务器端源代码如下:
#include<stdlib.h>
#include<stdio.h>
#include<netdb.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<fcntl.h>
#define MAXDATASIZE 256
#define SERVPORT 4444
#define BACKLOG 10
#define STDIN 0
int main(void)
{
FILE * fp;
int sockfd,client_fd;
int sin_size;
struct sockaddr_in my_addr,remote_addr;
char buf[256];
char buff[256];
char send_str[256];
int recvbytes;
fd_set rfd_set,wfd_set,efd_set;
struct timeval timeout;
int ret;
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
perror("socket");
exit(1);
}
bzero(&my_addr,sizeof(struct sockaddr_in));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(SERVPORT);
inet_aton("127.0.0.1",&my_addr.sin_addr);
if(bind(sockfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr)) == -1)
{
perror("bind error");
exit(1);
}
if(listen(sockfd,BACKLOG) == -1)
{
perror("listen error");
exit(1);
}
sin_size = sizeof(struct sockaddr_in);
if((client_fd = accept(sockfd,(struct sockaddr *)&remote_addr,&sin_size)) == -1)
{
perror("accpet error");
exit(1);
}
fcntl(client_fd,F_SETFD,O_NONBLOCK);
recvbytes = recv(client_fd,buff,MAXDATASIZE,0);
buff[recvbytes] = '\0';
fflush(stdout);
if((fp = fopen("name.txt","a+")) == NULL)
{
printf("can not open file,exit...\n");
return -1;
}
fprintf(fp,"%s\n",buff);
while(1)
{
FD_ZERO(&rfd_set);
FD_ZERO(&wfd_set);
FD_ZERO(&efd_set);
FD_SET(STDIN,&rfd_set);
FD_SET(client_fd,&rfd_set);
FD_SET(client_fd,&wfd_set);
FD_SET(client_fd,&efd_set);
timeout.tv_sec = 10;
timeout.tv_usec = 0;
ret = select(client_fd + 1,&rfd_set,&wfd_set,&efd_set,&timeout);
if(ret == 0)
{
continue;
}
if(ret < 0)
{
perror("select error:");
exit(-1);
}
if(FD_ISSET(STDIN,&rfd_set))
{
fgets(send_str,256,stdin);
send_str[strlen(send_str)-1] = '\0';
if(strncmp("quit",send_str,4) == 0)
{
close(client_fd);
close(sockfd);
exit(0);
}
send(client_fd,send_str,strlen(send_str),0);
}
if(FD_ISSET(client_fd,&rfd_set))
{
recvbytes = recv(client_fd,buf,MAXDATASIZE,0);
if(recvbytes == 0)
{
close(client_fd);
close(sockfd);
exit(0);
}
buf[recvbytes] = '\0';
printf("%s:%s\n",buff,buf);
printf("Server: ");
fflush(stdout);
}
if(FD_ISSET(client_fd,&efd_set))
{
close(client_fd);
exit(0);
}
}
}
客户端源程序如下:
#include<stdlib.h>
#include<stdio.h>
#include<netdb.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<fcntl.h>
#define SERVPORT 4444
#define MAXDATASIZE 256
#define STDIN 0
int main(void)
{
int sockfd;
int recvbytes;
char buf[MAXDATASIZE];
char * str;
char name[MAXDATASIZE];
char send_str[MAXDATASIZE];
struct sockaddr_in serv_addr;
fd_set rfd_set,wfd_set,efd_set;
struct timeval timeout;
int ret;
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
perror("socket error");
exit(1);
}
bzero(&serv_addr,sizeof(struct sockaddr_in));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERVPORT);
inet_aton("127.0.0.1",&serv_addr.sin_addr);
if(connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr)) == -1)
{
perror("connect error");
exit(1);
}
fcntl(sockfd,F_SETFD,O_NONBLOCK);
printf("要聊天首先输入你的名字:");
scanf("%s",name);
name[strlen(name)] = '\0';
printf("%s: ",name);
fflush(stdout);
send(sockfd,name,strlen(name),0);
while(1)
{
FD_ZERO(&rfd_set);
FD_ZERO(&wfd_set);
FD_ZERO(&efd_set);
FD_SET(STDIN,&rfd_set);
FD_SET(sockfd,&rfd_set);
FD_SET(sockfd,&efd_set);
timeout.tv_sec = 10;
timeout.tv_usec = 0;
ret = select(sockfd+1,&rfd_set,&wfd_set,&efd_set,&timeout);
if(ret == 0)
{
continue;
}
if(ret < 0)
{
perror("select error: ");
exit(-1);
}
if(FD_ISSET(STDIN,&rfd_set))
{
fgets(send_str,256,stdin);
send_str[strlen(send_str)-1] = '\0';
if(strncmp("quit",send_str,4) == 0)
{
close(sockfd);
exit(0);
}
send(sockfd,send_str,strlen(send_str),0);
}
if(FD_ISSET(sockfd,&rfd_set))
{
recvbytes = recv(sockfd,buf,MAXDATASIZE,0);
if(recvbytes == 0)
{
close(sockfd);
exit(0);
}
buf[recvbytes] = '\0';
printf("Server: %s\n",buf);
printf("%s: ",name);
fflush(stdout);
}
if(FD_ISSET(sockfd,&efd_set))
{
close(sockfd);
exit(0);
}
}
}
运行结果如下: