一 理论知识
1.1 概念以及socket种类
socket也叫“套接字”,虽然但是吧这个翻译其实很不好,不利于初学者理解,socket可以理解成计算机提供给程序员的接口,数据在客户端和服务端之间的socket之间传输。socket把复杂的TCP/IP协议封装,对于程序员来说只要利用好函数,就可以实现数据通信。 TCP提供了stream和datagram两种通信机制,所以socket分这两种。 stream的类型是SOCK_STREAM,采用TCP协议,TCP协议在计算机网络中是安全可靠的有连接的协议。datagram的类型是SOCK_DGRAM,采用的是UDP协议,UDP是不可靠的协议,现在在实际应用开发中,主要采用的是TCP。
二 server 端通信过程
- server端将一个套接字bind到指定的ip地址和port,并且通过该套接字等待和监听客户的连接请求
- 客户程序向服务端程序绑定的地址和端口发送连接请求
- 服务端接收请求
- server和client通过socket进行通信
服务端工作流程 代码编写如下,一步一步详细解答。 头文件如下
#include<stdio.h>
#include<iostream>
#include<cstring>
#include<sys/fcntl.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<errno.h>
#include<sys/types.h>
#include <arpa/inet.h>
第一步 为服务端创建socket
int listenfd;
listenfd=socket(AF_INET,SOCK_STREAM,0);
if(listenfd==-1)
{
printf("socket create fail\n");
return -1;
}
第二步 将socket绑定到提供的ip的地址和对应的端口上
struct sockaddr_in serveraddr;
memset(&serveraddr,0,sizeof(serveraddr));
serveraddr.sin_family=AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port=htons(atoi(argv[1]));
if(bind(listenfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr))!=0)
{
printf("bind failed \n");
return -1;
}
argv[1]是运行可执行文件时,后面提供的参数。
第三步 利用listen函数使用主动连接套接口变为被连接套接口
使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。
if(listen(listenfd,5)!=0)
{
printf("Listen failed\n");
close(listenfd);
return -1;
}
函数声明
int listen(int sockfd, int backlog);
参数sockfd是已经被bind过的socket。socket函数返回的socket是一个主动连接的socket,在服务端的编程中,程序员希望这个socket可以接受外来的连接请求,也就是被动等待客户端来连接。由于系统默认时认为一个socket是主动连接的,所以需要通过某种方式来告诉系统,程序员通过调用listen函数来完成这件事。 参数backlog,这个参数涉及到一些网络的细节,比较麻烦,填5、10都行,一般不超过30。当调用listen之后,服务端的socket就可以调用accept来接受客户端的连接请求。
第四步 接受客户端的请求
服务端接受客户端的请求
int clintfd;
int socklen=sizeof(struct sockaddr_in);
struct sockaddr_in client_addr;
clintfd=accept(listenfd,(struct sockaddr*)&client_addr,(socklen_t *)&socklen);
if(clintfd==-1)
printf("connect failed\n");
else
printf("client %s has connnected\n",inet_ntoa(client_addr.sin_addr));
其中accept函数如下
第五步 同客户端连接,接受数据
int iret;
memset(buf,0,sizeof(buf));
iret=recv(clintfd,buf,strlen(buf),0);
if(iret<=0)
{
perror("send");
break;
}
printf("receive %s\n",buf);
recv函数用于server端socket传送过来的数据,函数声明如下:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
这时候这里的sockfd其实是clientfd,buf中存储接受的数据,如果client的socket没有发送数据,recv函数就会一直等待,如果发送了数据,函数返回接受到的字符数,如果socket关闭那么就会返回0
char buffer[1024];
while (1)
{
int iret;
memset(buffer,0,sizeof(buffer));
iret=recv(clintfd,buffer,sizeof(buffer),0);
if (iret<=0)
{
printf("iret=%d\n",iret); break;
}
printf("receive :%s\n",buffer);
strcpy(buffer,"ok");
if ( (iret=send(clintfd,buffer,strlen(buffer),0))<=0)
{
perror("send");
break;
}
printf("send :%s\n",buffer);
}
send函数用于把数据通过socket发送给对端。不论是客户端还是服务端,应用程序都用send函数来向TCP连接的另一端发送数据。
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
返回值是0的时候,通信已经中断。
第六步 关闭连接
close(listenfd);
close(clintfd);
代码附录 server端
#include<stdio.h>
#include<iostream>
#include<cstring>
#include<stdlib.h>
#include<sys/fcntl.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<errno.h>
#include<sys/types.h>
#include <arpa/inet.h>
using namespace std;
int main(int argc,char *argv[])
{
int listenfd;
listenfd=socket(AF_INET,SOCK_STREAM,0);
if(listenfd==-1)
{
printf("socket create fail\n");
return -1;
}
struct sockaddr_in serveraddr;
memset(&serveraddr,0,sizeof(serveraddr));
serveraddr.sin_family=AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port=htons(atoi(argv[1]));
if(bind(listenfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr))!=0)
{
printf("bind failed \n");
return -1;
}
if(listen(listenfd,5)!=0)
{
printf("Listen failed\n");
close(listenfd);
return -1;
}
int clintfd;
int socklen=sizeof(struct sockaddr_in);
struct sockaddr_in client_addr;
clintfd=accept(listenfd,(struct sockaddr*)&client_addr,(socklen_t *)&socklen);
if(clintfd==-1)
printf("connect failed\n");
else
printf("client %s has connnected\n",inet_ntoa(client_addr.sin_addr));
char buffer[1024];
while (1)
{
int iret;
memset(buffer,0,sizeof(buffer));
iret=recv(clintfd,buffer,sizeof(buffer),0);
if (iret<=0)
{
printf("iret=%d\n",iret); break;
}
printf("receive :%s\n",buffer);
strcpy(buffer,"ok");
if ( (iret=send(clintfd,buffer,strlen(buffer),0))<=0)
{
perror("send");
break;
}
printf("send :%s\n",buffer);
}
close(listenfd); close(clintfd);
}
client端通信过程
流程如下
所需头文件如下
#include <unistd.h>
#include <stdlib.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
第一步 为客服端创建socket
int sockfd;
sockfd = socket(AF_INET,SOCK_STREAM,0))
第二步 向server发起连接请求
struct hostent* h;
if ( (h = gethostbyname(argv[1])) == 0 )
{
printf("gethostbyname failed.\n");
close(sockfd);
return -1;
}
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2]));
memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);
if (connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr)) != 0)
{
perror("connect");
close(sockfd);
return -1;
}
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2]));
servaddr.sin_addr.s_addr=inet_addr(argv[1]);
if (connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr)) != 0)
{
perror("connect");
close(sockfd);
return -1;
}
其中用到的结构体和函数介绍如下
struct hostent//
struct hostent
{
char *h_name;
char **h_aliases;
int h_addrtype;
int h_length;
char **h_addr_list;
};
#define h_addr h_addr_list[0]
这里面使用的gethostbyname函数,只能用在客户端上,主要作用就是把字符串的IP地址转换成结构体的ip地址。
第三步 向server发送数据
while(1){
char buffer[1024];
int iret;
int choice;
printf("if you want to continue to chat please input 1,or input 2\n");
scanf("%d\n",&choice);
if(choice==2)
break;
memset(buffer,0,sizeof(buffer));
printf("please input what you want to say\n");
scanf("%s",buffer);
if ( (iret=send(sockfd,buffer,strlen(buffer),0))<=0)
{
perror("send");
break;
}
printf("send:%s\n",buffer);
memset(buffer,0,sizeof(buffer));
if ( (iret=recv(sockfd,buffer,sizeof(buffer),0))<=0)
{
printf("iret=%d\n",iret);
break;
}
printf("receive:%s\n",buffer);
}
}
第四步 关闭连接释放资源
close(sockfd);
代码附录 clinet端
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include<string>
#include<iostream>
using namespace std;
int main(int argc,char *argv[])
{
int sockfd;
if ( (sockfd = socket(AF_INET,SOCK_STREAM,0))==-1)
{
perror("socket");
return -1;
}
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2]));
servaddr.sin_addr.s_addr=inet_addr(argv[1]);
if (connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr)) != 0)
{
perror("connect");
close(sockfd);
return -1;
}
char buffer[1024];
while(1)
{
int iret;
memset(buffer,0,sizeof(buffer));
int choice;
printf("if you want to continue to chat please input 1 or input else\n");
scanf("%d",&choice);
getchar();
if(choice!=1)
break;
else
{
printf("please input what you want to say\n");
cin.getline(buffer,1024);
}
if ( (iret=send(sockfd,buffer,strlen(buffer),0))<=0)
{
perror("send");
break;
}
printf("send: %s\n",buffer);
memset(buffer,0,sizeof(buffer));
if ( (iret=recv(sockfd,buffer,sizeof(buffer),0))<=0)
{
printf("iret=%d\n",iret);
break;
}
printf("receive: %s\n",buffer);
}
close(sockfd);
}
三 总结
TCP服务器端依次调用socket()、 bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
对于服务端来说有两个socket,这里该如何理解呢?
首先在linux内核中,socket函数创建的都是主动套接字,但是服务端在经过listen()以后呢,会转换成被动socket,经过accept函数处理后又会变成已连接的socket,在成为已连接socket之前,还是同一个socket,但是这里面状态发生了改变,服务端经过accept之后的socket是新的socket,用于连接之后的读写操作。 监听socket,是服务器作为客户端连接请求的一个对端,只需创建一次能够让客户端请求到有这个端点就ok,所以监听socket(listen_socket_fd)存在于服务器的整个生命周期, 不需要每个连接都创建一个新的监听socket出来, 没必要呢。已连接socket(connect_socket_fd)是客户端与服务器之间已经建立起来了的连接的一个端点,服务器每次接受连接请求时都会创建一个新已连接socket,它的生命周期只是客户端请求服务端的时间范围内。
|