注:部分截图没给出,转载需说明出处,仅供学习。
1.编写服务端与客户端程序
(1)基本通信编程
客户端要求:
指定客户端的IP地址和端口号
与服务端建立TCP连接
请求读取文件A(全部小写字母,多行),并将A文件发送给服务器端
显示本地和异地协议地址信息
服务端要求:
指定服务器端端口号,使用通配IP地址,监听TCP端口
处理客户端的TCP连接请求
接受客户端转换请求(将发送的A文件接受后转换全部大写字母),并返回给客户端。
显示本地和异地协议地址信息
总结网络编程中的注意事项
主要思路:客户端读取保存在客户端的文件(这里是test0.txt),并将里面的内容发送给服务器端,服务器端接受后转换成大写并返回给客户端。
头文件:(下面的所有代码头文件不再列出)
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include /*for toupper*/
#include
主要代码:
客户端(shiyan2.1c.c):
/* client端:./client <上传文件名>*/
#define MAXLINE 1024
void usage(char *command)//没有输入文件名时报错
{
printf("usage :%s filename\n", command);
exit(0);
}
int main(int argc, char *argv[])
{
int sock_id;
int len;
struct sockaddr_in serv_addr; //服务器端网络地址结构体
struct sockaddr_in my_addr;//本地地址
char buf[BUFSIZ]; //数据传送的缓冲区
FILE *fp;
if (argc != 2) { /*输入参数*/
usage(argv[0]);
}
if ((fp = fopen(argv[1],"r")) == NULL) { /*输入的 上传文件名*/
perror("Open file failed\n");
exit(0);
}
/*创建客户端套接字*/
if((sock_id=socket(PF_INET,SOCK_STREAM,0))<0) {
perror("socket"); /*返回-1 出错*/
return 1;
}
//初始化
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_port= htons(8000);
serv_addr.sin_addr.s_addr=inet_addr("127.0.0.1");
/*连接*/
if(connect(sock_id,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr))<0){
perror("connect");
return 1;
}
printf("connected to server\n");
if((len=recv(sock_id,buf,BUFSIZ,0))>0){
buf[len]='\0';
printf("%s",buf); }
socklen_t l;
l=sizeof(my_addr);
if(getsockname(sock_id,(struct sockaddr*)&my_addr,&len)){
printf("getsockname error\n");
return(-1);
}
printf("client addr: IP:%s port:%d \n",inet_ntoa(my_addr.sin_addr),ntohs(my_addr.sin_port));
l=sizeof(serv_addr);
if(getpeername(sock_id,(struct sockaddr*)&serv_addr,&l))
{
printf("getpeername error\n");
return(-1);
}
printf("server addr: IP:%s port:%d \n",inet_ntoa(serv_addr.sin_addr),ntohs(serv_addr.sin_port));
/*读取文件信息并发送给服务器端*/
bzero(buf, MAXLINE);
while ((len = fread(buf, sizeof(char), MAXLINE, fp)) >0 ) {
if ( (send(sock_id, buf, len, 0))< 0 ) {
perror("Send file failed\n");
exit(0);
}
bzero(buf, MAXLINE);
}
fclose(fp);
bzero(buf, MAXLINE);
len=recv(sock_id,buf,BUFSIZ,0); //接受服务器端的数据
buf[len]='\0';
printf("received:%s\n",buf);
close(sock_id);//关闭套接字
return 0;
}
服务端(shiyan2.1s.c):
int main(int argc, char *argv[]){
int server_sockfd;//服务器端套接字
int client_sockfd;//客户端套接字
int len,i;
struct sockaddr_in my_addr; //服务器网络地址结构体
struct sockaddr_in remote_addr; //客户端网络地址结构体
int sin_size;
char buf[BUFSIZ];
memset(&my_addr,0,sizeof(my_addr)); //数据初始化--清零
my_addr.sin_family=AF_INET;
my_addr.sin_addr.s_addr=INADDR_ANY;
my_addr.sin_port=htons(8000);
/*创建服务器端套接字*/
printf("creating socket...\n");
if((server_sockfd=socket(PF_INET,SOCK_STREAM,0))<0){
perror("socket");
return 1;
}
int on = 1;
if((setsockopt(server_sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)))<0) {
perror("setsockopt failed");
exit(1);
}
/*绑定*/
if (bind(server_sockfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr))<0){
perror("bind");
return 1;
}
/*监听连接请求--监听队列长度为5*/
listen(server_sockfd,5);
sin_size=sizeof(struct sockaddr_in);
/*等待客户端连接请求到达*/
if((client_sockfd=accept(server_sockfd,(struct sockaddr *)&remote_addr,&sin_size))<0){
perror("accept");
return 1;
}
printf("accept client %s\n",inet_ntoa(remote_addr.sin_addr));
struct sockaddr_in sockn,peern;
socklen_t le;
le=sizeof(sockn);
if(getsockname(server_sockfd,(struct sockaddr*)&sockn,&le)){
printf("getsockname error\n");
return(-1);
}
printf("server addr: IP:%s port:%d \n",inet_ntoa(sockn.sin_addr),ntohs(sockn.sin_port));
socklen_t l;
l=sizeof(peern);
if(getpeername(client_sockfd,(struct sockaddr*)&peern,&l)){
printf("getpeername error\n");
return(-1);
}
printf("client addr: IP:%s port:%d \n",inet_ntoa(peern.sin_addr),ntohs(peern.sin_port));
len=send(client_sockfd,"message from server : Welcome to my server\n",43,0);//发送欢迎信息
/*接收客户端的数据并将其发送给客户端*/
while((len=recv(client_sockfd,buf,BUFSIZ,0))>0){
buf[len]='\0';
printf("%s\n",buf);
for (i = 0; i < len; i++)
buf[i] = toupper(buf[i]);//用来将字符c转换为大写英文字母
printf("toupper: %s\n",buf);
if(send(client_sockfd,buf,len,0)<0){
perror("write");
return 1;
}
}
close(client_sockfd);
close(server_sockfd);
return 0;
}
test0.txt文件内容:
运行结果:(分别为客户端和服务器端)
在网络编程中的注意事项:
1)在编程前,要熟悉各个函数的用法以及参数,才能在编程时更灵活的使用他们。
2)使用文件指针fp时,对文件读完/写完要及时关闭,否则会导致数据丢失。
3)在服务器端可以使用setsockopt函数,可以重用socket,方便了编程过程中多次编译。
4)在使用ssh连接一个linux系统时(我使用的是kali),登录时不能登录系统的root账户,其他账户才可以。
5)服务器端的IP地址初始化一定要是INADDR_ANY,才能够接收到任何IP地址的客户端连接。
6)编译时,要将服务器先开启,后开客户端。
(2)多连接编程
客户端:获取两个随机数,分别发送给两个服务器;并扩展到多服务器;多个套接口向多个服务器请求服务。
服务端:两个服务器,1个将随机数相加,一个将随机数相乘,分别返回给客户端;扩展到多个服务器。
主要思路:客户端使用数组创建多个socket,手动输入两个随机数发送给两个服务器端,服务端计算后并返回。
主要代码:
客户端:(dljcli.c)
#define NUMBER 2
int main(int argc,char *argv[]){
int sockfd[NUMBER]; /*NUMBER为需要建立的套接字数量*/
struct sockaddr_in servaddr[NUMBER];//多个服务器端网络地址结构体
char buf[1024];
int i,len;
/*创建客户端套接字--IPv4协议,面向连接通信,TCP协议*/
for(i=0;i
服务端(*):(shiyan2.21.c)
int main(int argc, char *argv[])
{
int server_sockfd;//服务器端套接字
int client_sockfd;//客户端套接字
int len,i;
struct sockaddr_in my_addr; //服务器网络地址结构体
struct sockaddr_in remote_addr; //客户端网络地址结构体
int sin_size;
char buf[BUFSIZ]; //数据传送的缓冲区
memset(&my_addr,0,sizeof(my_addr)); //数据初始化--清零
my_addr.sin_family=AF_INET; //设置为IP通信
my_addr.sin_addr.s_addr=INADDR_ANY;//服务器IP地址--允许连接到所有本地地址上
my_addr.sin_port=htons(8000); //服务器端口号
/*创建服务器端套接字--IPv4协议,面向连接通信,TCP协议*/
if((server_sockfd=socket(PF_INET,SOCK_STREAM,0))<0){
perror("socket");
return 1;
}
/*将套接字绑定到服务器的网络地址上*/
if (bind(server_sockfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr))<0){
perror("bind");
return 1;
}
/*监听连接请求--监听队列长度为5*/
listen(server_sockfd,10);
sin_size=sizeof(struct sockaddr_in);
/*等待客户端连接请求到达*/
if((client_sockfd=accept(server_sockfd,(struct sockaddr *)&remote_addr,&sin_size))<0){
perror("accept");
return 1;
}
printf("accept client %s\n",inet_ntoa(remote_addr.sin_addr));
int num;
/*接收客户端的数据并将其发送给客户端--recv返回接收到的字节数,send返回发送的字节数*/
while((len=recv(client_sockfd,buf,BUFSIZ,0))>0){
buf[len]='\0';
num=atoi(buf); /*将字符串转换为整型值。*/
printf("translation: %d * %d = ",num,num);
num=num*num;
printf("%d\n",num);
sprintf(buf,"%d",num);
//itoa(num,buf,10); /*将整型值转换为字符串。linux下没有这个函数*/
len=strlen(buf);
buf[len]='\0';
printf("send answer: %s\n",buf);
if(send(client_sockfd,buf,len,0)<0){
perror("write");
return 1;
}
}
close(client_sockfd);
close(server_sockfd);
return 0;
}
服务端(+):(shiyan2.22.c)
int main(int argc, char *argv[]){
int server_sockfd;//服务器端套接字
int client_sockfd;//客户端套接字
int len,i;
struct sockaddr_in my_addr; //服务器网络地址结构体
struct sockaddr_in remote_addr; //客户端网络地址结构体
int sin_size;
char buf[BUFSIZ]; //数据传送的缓冲区
memset(&my_addr,0,sizeof(my_addr)); //数据初始化--清零
my_addr.sin_family=AF_INET; //设置为IP通信
my_addr.sin_addr.s_addr=INADDR_ANY;//服务器IP地址--允许连接到所有本地地址上
my_addr.sin_port=htons(8001); //服务器端口号
/*创建服务器端套接字--IPv4协议,面向连接通信,TCP协议*/
if((server_sockfd=socket(PF_INET,SOCK_STREAM,0))<0){
perror("socket");
return 1;
}
/*将套接字绑定到服务器的网络地址上*/
if(bind(server_sockfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr))<0){
perror("bind");
return 1;
}
/*监听连接请求--监听队列长度为5*/
listen(server_sockfd,10);
sin_size=sizeof(struct sockaddr_in);
/*等待客户端连接请求到达*/
if((client_sockfd=accept(server_sockfd,(struct sockaddr *)&remote_addr,&sin_size))<0){
perror("accept");
return 1;
}
printf("accept client %s\n",inet_ntoa(remote_addr.sin_addr));
int num;
/*接收客户端的数据并将其发送给客户端--recv返回接收到的字节数,send返回发送的字节数*/
while((len=recv(client_sockfd,buf,BUFSIZ,0))>0){
buf[len]='\0';
num=atoi(buf); /*将字符串转换为整型值。*/
printf("translation: %d + %d = %d\n",num,num,num+num);
num=num+num;
sprintf(buf,"%d",num);
//itoa(num,buf,10); /*将整型值转换为字符串。linux下没有这个函数*/
printf("send answer: %s\n",buf);
if(send(client_sockfd,buf,len,0)<0){
perror("write");
return 1;
}
}
close(client_sockfd);
close(server_sockfd);
return 0;
}
运行结果:
客户端:
服务端(*):
服务端(+):
上面所示的是一个客户端两个服务端的情况,如果要拓展为n个服务端的话,则只需把客户端程序中这条命令“#define NUMBER 2”中的2改成n就可以。
(3)多路复用
将上述(1)(2)功能实现,改用多路复用I/O来实现。客户端(标准输入/读文件,与网络数据到达进行多路复用)每读取一行便发送消息,服务器端(监听套接口与已连接套接口的复用)每次转换一行信息。
主要思路:一个客户端,两个服务端,两个服务端都使用多路复用I/O,一个对这个客户端送来的随机数相加,另一个相乘并返回。客户端每输入一行,服务器端就处理一行。(多路复用思想:当前进程可以处理多个响应事件,记录多个描述符,然后控制轮询时间态,当有响应产生的时候就去保存当前响应文件描述符,对他进行连接处理/数据传输。)
主要代码:
客户端:(shiyan2.3c.c)
客户端的代码与上面(2)多连接编程的客户端代码类似,主要不同的地方是:
i=0;
while(n!=0){
while(1)//这一步是为了让“客户端输入一行,服务器端转换一行信息”效果更明显
{
printf("Enter string to send:");
scanf("%s",buf);
if(!strcmp(buf,"quit"))
break;
write(sockfd[i],buf,strlen(buf));
if((read(sockfd[i],buf,1024))>0)
printf("received from server %d : %s\n",i,buf);
}
i++;
bzero(buf, 1024);
n--;
}
(更改定义的NUMBER值也可以实现多个套接口向多个服务器请求服务)
服务端:(shiyan2.31.c)
#define _BACKLOG_ 5
#define rep(i,a,b) for(i=a;i0){
FD_SET(fds[i],&reads);
if(fds[i] > max_fd){ //获取最大的文件描述符值
max_fd = fds[i];
}
}
}
switch(select(max_fd+1, &reads,&writes,NULL,&timeout)){
case 0 ://timeout
{
printf("select timeout\n");
break;}
case -1:
{ //error
perror("select");
break;}
default://返回改变了的文件描述.
{
char buf[1024];
//遍历所有的文件描述符集合。
rep(i,0,fds_num){
//确认是否时监听时间,是的话就绪要accept;
if(fds[i] == sockfd && \
FD_ISSET(fds[i],&reads)){
printf("begin accepting...\n");
new_sock = accept(sockfd,(struct sockaddr*)&client,&len);
if(new_sock <0){
perror("accept");
continue;
}
printf("get a new connet...%d\n",new_sock);
rep(i,0,fds_num){
if(fds[i] == -1){
fds[i] = new_sock;
break;
}
}
if(i == fds_num){
close(new_sock);
}
}
else if(fds[i] > 0 &&\
FD_ISSET(fds[i],&reads)){ //正常事件,但是是非监听时间,也就代表时新建立的new_sock。
ssize_t s = read(fds[i],buf,sizeof(buf) -1);
if(s > 0){
buf[s] = '\0';
printf("receive data from client : %s\n",buf);
FD_SET(fds[i],&writes);
}
else if(s == 0){
printf("client quit...\n");
sleep(5);
close(fds[i]);
fds[i] = -1;
}
else{}
}
else{}
if(fds[i] > 0&&\
FD_ISSET(fds[i],&writes)){
if(IsInt(buf)){
num=atoi(buf); /*将字符串转换为整型值。*/
printf("translation: %d * %d = ",num,num);
num=num*num;
printf("%d\n",num);
sprintf(buf,"%d",num);
printf("send answer: %s\n",buf);
}
else if(!IsInt(buf)){
int len=strlen(buf);
for (j = 0; j < len; j++)
buf[j] = toupper(buf[j]);
printf("send answer(upper): %s\n",buf);
}
write(fds[i],buf,sizeof(buf));
}
}
}
break;
}
}
return 0;
}
运行结果:
客户端:
分析:客户端可以向一个服务器端连续发送数据,每发送一行,就收到一行(服务器端处理后发回的),输入quit即可退出,然后就可以向另一个服务器端发送数据(这里是同一个服务器,客户端通过不同的套接口与同一个服务器通信)。
服务端:
分析:服务器端接受一行客户端发来的数据就处理一行并返回。这里的服务器提供两个服务,客户端发来的如果是数字,则计算这个数字自身相乘的结果并返回,如果是字符串则将其转为大写并返回。
2.采用netstat命令,以及sleep系统调用,验证TCP连接建立的三次握手各种状态和终止的几种状态。
监听状态,已连接状态,半关闭状态,完全关闭状态等。
(此处观察的是(3)多路复用中的TCP连接建立过程,下面只列出其中一个服务器(端口号:8000))
(1)LISTEN(监听状态):服务器端先开启,客户端还没开启的时候,服务器端是监听状态。
(2)ESTABLISHED(已连接状态):当客户端与服务器端完成三次握手,成功建立连接,客户端和服务器端都是已连接状态,双方就可以发送数据进行通信了。
客户端:
服务器端:
分析:客户端调用connect函数请求TCP连接时,系统会自动为它选择一个未用端口号,并用本地的IP地址设置套接字地址的相应项,如上图可知,系统给这个客户端选择了50488端口。(grep 指定端口8000)
netstat扫描的结果:可以看到服务器端由监听状态变为已连接状态。
(3)FIN_WAIT2:主动方(主动关闭的一方)接收到对方的ACK,将状态转入FIN_WAIT2。
分析:上图是服务器端先发起断开连接请求。主动方(服务器端)发起断开连接请求等待对方确认,将自身的状态转入FIN-WAIT1。被动方(客户端)收到FIN报文,回复ACK,将状态转为CLOSE_WAIT。主动方(服务器端)接收到对方的ACK,将状态转入FIN_WAIT2。
(4)CLOSE_WAIT:被动方收到FIN报文,回复ACK,将状态转为CLOSE_WAIT。如上图,客户端收到服务器端的断开连接请求。
(5)TIME_WAIT:(客户端先发起断开连接的情况)客户端收到了服务器端的FIN,将tcp连接状态转为TIME_WAIT。
因为感觉看到的状态不是很多,于是我用tcpdump再观察了一下:
客户端与两个服务器端的TCP连接过程:
客户端先发起断开连接:
3.捕获各个TCP连接传输数据过程中的各种信息
采用winpcap和ethereal软件进行捕获,并记录和解释捕获得到的信息
由于我在ethereal里没找到本机虚拟机地址,所以我是采用wireshark软件对(1)基本通信编程中的客户端和服务器端之间的通信进行捕获,如下:
我们可以从中很清楚的看到客户端与服务器端之间的通信。客户端端口号为58952,服务器端端口号为8000。
(1)建立连接(三次握手)
1)58952->8000 [SYN](报文编号No.3):客户端向服务器端发送同步请求SYN,附序列号Seq=0,并进入SYN_SENT状态,等待服务器确认。
2)8000->58952 [SYN,ACK](No.4):服务器收到SYN包,必须确认客户的SYN(ACK=1),同时自己也发送一个SYN包(Seq=0),询问客户端是否准备好进行数据通信,即SYN+ACK包,此时服务器进入SYN_RECV状态;
3)58952->8000 [ACK](No.5):客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ACK=1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。
完成三次握手,客户端与服务器开始传送数据。(根据代码我们也可以知道如下:)
4)8000->58952 [PSH,ACK](No.6):服务器端像客户端发送欢迎消息“welcome to my server”
我们也可以从捕获的报文中看到:
5)58952->8000 [PSH,ACK](No.8):客户端向服务器端发送test0.txt文件里的内容(小写字符串
6)8000->58952 [PSH,ACK](No.10):服务器端向客户端发送转换后的字符串
(2)连接终止
1)58952->8000 [FIN,ACK](No.11):客户端调用close,主动关闭,发送FIN,表示数据发送完毕,并且进入FIN_WAIT状态。(此处有个ACK是对上条服务器发来的数据的应答)
2)8000->58952 [ACK](No.12):服务器收到FIN,被动关闭,返回ACK,进入CLOSE_WAIT状态。
3)58952->8000 [FIN](No.13):服务器调用close关闭它的套接口,并发送一个FIN。
4)8000->58952 [ACK](No.14):客户端收到FIN并确认,发送ACK。
这个实验做得不是很好,因为是第一次做,所以在思考问题方面还有所欠缺。再后来也有发现更好的做法,但没去修改代码,如果读者有发现什么问题,欢迎提出。