一、TCP/IP 协议简介
1.1、TCP/IP 是基于 TCP 和 IP 这两个最初的协议之上的不同的通信协议的大的集合。
1.2、P协议 负责把数据从一台计算机通过网络发送到另一台计算机。数据被分割成一小块一小块,然后通过IP包发送出去。由于互联网链路复杂,两台计算机之间经常有多条线路,因此,路由器就负责决定如何把一个IP包转发出去。IP包的特点是按块发送,途径多个路由,但不保证能到达,也不保证顺序到达。
1.3、TCP协议 则是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。TCP协议会通过握手建立连接,然后,对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。
1.4、许多常用的更高级的协议都是建立在TCP协议基础上的,比如用于浏览器的HTTP协议、发送邮件的SMTP协议等。
1.5、一个TCP报文除了包含要传输的数据外,还包含源IP地址和目标IP地址,源端口和目标端口。
二、Socket服务器和客户端的开发步骤
2.1、创建套接字
2.2、为套接字添加信息(IP地址和端口号)
2.3、监听网络连接
2.4、监听到有客户端接入,接受一个连接
2.5、数据交互
2.6、关闭套接字,断开连接
三、socket编程使用相关API
我们深谙信息交流的价值,那网络中进程之间如何通信,如我们每天打开浏览器浏览网页 时,浏览器的进程怎么与web服务器通信的?当你用QQ聊天时,QQ进程怎么与服务器或你好友所在的QQ进程通信?这些都得靠socket?那什么是 socket?socket的类型有哪些?还有socket的基本函数,这些都是本文想介绍的。本文的主要内容如下:
1、网络中进程之间如何通信?
2、Socket是什么?
3、socket的基本操作
3.1、socket()函数
3.2、bind()函数
3.3、listen()、connect()函数
3.4、accept()函数
3.5、read()、write()函数等
3.6、close()函数
4、socket中TCP的三次握手建立连接详解
5、socket中TCP的四次握手释放连接详解
6、一个例子
1、网络中进程之间如何通信?
本地的进程间通信(IPC)有很多种方式,但可以总结为下面4类:
消息传递(管道、FIFO、消息队列)
同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
共享内存(匿名的和具名的)
远程过程调用(Solaris门和Sun RPC)
但这些都不是本文的主题!我们要讨论的是网络中进程之间如何通信?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是我为什么说“一切皆socket”。
3.1、socket()函数:
int socket(int domain, int type, int protocol);
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:
demain:即协议域,又称为协议族(family)系统使用的底层协议族,通常为AF_INET,表示互联网协议族(TCP/IP协议族);AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
AF_INET IPv4因特网域
AF_INEI6 IPv6 因特网域
AF_UNIX Unix域
AF_ROUTE 路由套接字
AF_KEY 密钥套接字
AF_UNSPEC 未指定
type:指定socket服务类型。
SOCK_STREAM(流服务,表示传输层使用TCP)
SOCK_UGRAM(数据报,表示传输层使用UDP),
SOCK_RAM:允许程序使用底层协议,原始套接字允许对底层协议如IP或ICMP进行访问,功能强大但不方便,主要由于协议的开发。
protocol:前两个参数构成的协议集合下,再选择一个具体的协议,一般都设置为0;常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议
返回值:成功返回非负整数, 它与文件描述符类似,我们把它称为套接口描述字,简称套接字。失败返回-1
**注意:**并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
3.2绑定socket()
正如上面所说bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
包含头文件<sys/socket.h>
功能:绑定一个本地地址到套接字
sockfd:socket函数返回的套接字,即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
– addr:是一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针,指向要绑定给socket的协议地址结构,这个地址结构根据地址创建socket时的地址协议族不同而不同
如ipv4对应的是:
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
};
struct in_addr {
uint32_t s_addr;
};
ipv6对应的是:
struct sockaddr_in6 {
sa_family_t sin6_family;
in_port_t sin6_port;
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_scope_id;
};
struct in6_addr {
unsigned char s6_addr[16];
};
Unix域对应的是:
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family;
char sun_path[UNIX_PATH_MAX];
};
– addrlen:地址长度
• 返回值:成功返回0,失败返回-1
struct sockaddr{
unisgned short as_family;//协议族
char sa_data[14];//IP+端口
};
同等替换:(常用)
struct sockaddr_in{
sa_family_t sin_family;//协议族
in_port_t sin_port; //端口号
struct in_addr sin_addr; //IP地址结构体
unsigned char sin_zero[8];//填充,没有实际意义,只是为跟sockaddr结构在内存中对齐,这样两者才能相互转换
};
int inet_aton(const char* straddr,struct in_addr *addrp);
//把字符串的“192.168.1.1”转为网络能识别的格式。
char inet_ntoa(struct in_addr inaddr);
//把网络格式的IP地址转为字符串形式。
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
3.3、监听socket
listen函数
• 包含头文件<sys/socket.h>
• 功能:将套接字用于监听进入的连接
• 原型
- int listen(int sockfd, int backlog);
• 参数
– sockfd:socket函数返回的套接字
– backlog:规定内核为此套接字排队的最大连接个数
• 返回值:成功返回0,失败返回-1;
3.4、accept函数
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
• 包含头文件<sys/socket.h>
• 功能:从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞。
• 原型
-int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
• 参数
– sockfd:服务器套接字
– addr:将返回对等方的套接字地址,用于返回客户端的协议地址
– addrlen:返回对等方的套接字地址长度
• 返回值:如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。,失败返回-1
注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
3.5、connect函数
• 包含头文件<sys/socket.h>
• 功能:建立一个连接至addr所指定的套接字
• 原型
-int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
• 参数
– sockfd:未连接套接字,即为客户端的socket描述字
– addr:要连接的套接字地址,服务器的socket地址
– addrlen:第二个参数addr长度
客户端通过调用connect函数来建立与TCP服务器的连接。
• 返回值:成功返回0,失败返回-1
3.6、write函数
• 包含头文件<unistd.h>
• 功能:向文件输出数据
• 原型
-ssize_t write(int fd,const void *buf,size_t nbytes)
• 参数
– fd:数据传输对象的文件描述符
– addr:保存要传输数据的缓冲地址值
– addrlen:要传输数据的字节数
• 返回值:成功返回写入的字节数,失败返回-1
3.7、read函数
• 包含头文件<unistd.h>
• 功能:用来接收数据
• 原型
-ssize-t read(int fd,void *buf,size_t nbytes);
• 参数
– fd:显示数据接收对象的文件描述符
– buf:要保存接收数据的缓冲地址值
– nbytes:要接收数据的最大字节数
• 返回值:成功返回接收的字节数,失败返回-1
3.8、close函数
• 包含头文件<unistd.h>
• 功能: 关闭文件或套接字
• 原型
-int close(int fd);
• 参数
– fd:需要关闭的文件或套接字的文件描述符
• 返回值:成功返回0,失败返回-1
3.9、fgets函数
• 包含头文件<stdio.h>
• 功能: 从文件流中读取一行或指定个数的字符
• 原型
-char *fgets(char *string,int size,FILE *stream);
• 参数
– string:字符数组,用来保存读取到的字符
– size:要读取的字符的个数,如果该行字符数大于size-1,则读到size-1个字符时结束,并在最后补充‘\0’;如果该行字符数小于等于size-1,则读取所有字符,并在最后补充‘\0’。
– stream :文件流指针
• 返回值:读取成功,返回读到的字符串,即string;失败返回NULL。
3.10、fputs函数
• 包含头文件<stdio.h>
• 功能:将指定的字符串写入到文件流中
• 原型
-int fputs(char *string,FILE *stream);
• 参数
– string:为将要写入的字符串
– stream:文件流指针
• 返回值:成功返回非负数,失败返回EOF
3.11、handler函数
• 包含头文件<stdio.h>
• 功能:特定事件发生时,操作系统向进程发送信息
• 原型
-void handler(int sig)
• 返回值:参数类型为int型,返回void型函数
3.12、signal函数
• 包含头文件<signal.h>
• 功能:获取系统产生各种信号并对此信号调用用户自己定义
• 原型
-void (* signal(int signo,void (*func)(int)))(int)
• 参数
– 第一个参数:指定信号的值
– 第二个参数:一个函数指针,用于指定针对信号的处理函数的函数内存空间地址
• 返回值:参数类型为int型,返回void型函数指针
3.13、fork函数
• 包含头文件<unistd.h>
• 功能:把Linux 变换为多任务系统的基础
• 原型
-pid_t fork(void)
• 返回值:成功时返回进程ID,失败时返回-1
3.14、kill函数
• 包含头文件<signal.h>
• 功能:发送指定的信号到相应进程
• 原型
-int kill(pid_t,int sig)
• 参数
– pid_t:参数进程标识号
– sig:进程号要传递信号值
• 返回值:成功返回0,失败返回-1
3.15、通信
TCP数据读写
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
recv读取sockfd上的数据,buf和len分别指定读缓冲区的位置和大小,flags通常为0。成功时返回实际读取到的数据长度,它可能小于期望的长度,因此需要多次调用recv;出错时返回-1并设置errno;
send往sockfd上写入数据,buf和len分别指定写缓冲区的位置和大小。成功时返回实际写入的数据长度,失败则返回-1并设置errno;
4.下面给出实现的一个实例
模拟ftp服务器
服务器端代码如下:
#include "config.h"
int getCmdType(char* cmd)
{
if(!strcmp("ls",cmd)) return LS;
if(!strcmp("quit",cmd)) return QUIT;
if(!strcmp("pwd",cmd)) return PWD;
if(strstr(cmd,"cd")!=NULL) return CD;
if(strstr(cmd,"get")!=NULL) return GET;
if(strstr(cmd,"put")!=NULL) return PUT;
return 100;
}
char* getDesDir(char* cmsg)
{
char* p;
p=strtok(cmsg," ");
p=strtok(NULL," ");
return p;
}
void* msgHandler(struct Msg msg,int fd)//客户端的接入
{
char dataBuf[1024]={0};
char* file=NULL;
int fdfile;
printf("cmd:%s\n",msg.data);
int ret=getCmdType(msg.data);
switch(ret)
{
case LS:
case PWD:
msg.type=0;
FILE* r=popen(msg.data,"r");
fread(msg.data,sizeof(msg.data),1,r);
write(fd,&msg,sizeof(msg));
break;
case CD:
msg.type=1;
char* dir=getDesDir(msg.data);
printf("dir:%s\n",dir);
chdir(dir);
break;
case GET:
file=getDesDir(msg.data);
if(access(file,F_OK)==-1)
{
strcpy(msg.data,"NO This File!");
write(fd,&msg,sizeof(msg));
}
else
{
msg.type=DOFILE;
fdfile=open(file,O_RDWR);
read(fdfile,dataBuf,sizeof(dataBuf));
close(fdfile);
strcpy(msg.data,dataBuf);
write(fd,&msg,sizeof(msg));
}
break;
case PUT:
fdfile=open(getDesDir(msg.data),O_RDWR|O_CREAT,0666);
write(fdfile,msg.secondBuf,strlen(msg.secondBuf));
close(fdfile);
break;
case QUIT:
printf("client qiut!\n");
exit(-1);
}
}
int main(int argc,char** argv)
{
int s_fd;
int c_fd;
int n_read;
char readBuf[128];
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
struct Msg msg;
if(argc!=3){
printf("param is not good!\n");
exit(-1);
}
memset(&s_addr,0,sizeof(struct sockaddr_in));
memset(&c_addr,0,sizeof(struct sockaddr_in));
//1、socket
s_fd=socket(AF_INET,SOCK_STREAM,0);
if(s_fd==-1)
{
perror("socket");
exit(-1);
}
s_addr.sin_family=AF_INET;
s_addr.sin_port=htons(atoi(argv[2]));
inet_aton(argv[1],&s_addr.sin_addr);
//bind
bind(s_fd,(struct sockaddr*)&s_addr,sizeof(struct sockaddr_in));
//listen
listen(s_fd,10);
//accept
int clen=sizeof(struct sockaddr_in);
while(1)
{
c_fd=accept(s_fd,(struct sockaddr*)&c_addr,&clen);
if(c_fd==-1)
{
perror("accept");
}
printf("get connect:%s\n",inet_ntoa(c_addr.sin_addr));
//1、当有客户端接入时创建一个子进程,去创建连接
//2、exec族函数;
if(fork()==0)//一个子进程负责一个连接通道
{
while(1)
{
memset(msg.data,0,sizeof(msg.data));
n_read=read(c_fd,&msg,sizeof(msg));
if(n_read==0)
{
printf("client out!\n");
break;
}
else if(n_read>0)
{
msgHandler(msg,c_fd);
}
}
}
}
close(c_fd);
close(s_fd);
return 0;
}
客户端代码:
#include "config.h"
char* getdir(char* cmd)
{
char* p;
p=strtok(cmd," ");
p=strtok(NULL," ");
return p;
}
int getCmdType(char* cmd)
{
if(strstr(cmd,"lcd")) return LCD;
if(!strcmp("quit",cmd)) return QUIT;
if(!strcmp("ls",cmd)) return LS;
if(!strcmp("lls",cmd)) return LLS;
if(!strcmp("pwd",cmd)) return LS;
if(strstr(cmd,"cd")) return CD;
if(strstr(cmd,"get")) return GET;
if(strstr(cmd,"put")) return PUT;
return -1;
}
int cmdHandler(struct Msg msg,int fd)
{
char buf[32];
char* dir=NULL;
int filefd;
int ret=getCmdType(msg.data);
switch(ret)
{
case LS:
case CD:
case PWD:
msg.type=0;
write(fd,&msg,sizeof(msg));
break;
case GET:
msg.type=2;
write(fd,&msg,sizeof(msg));
break;
case PUT:
strcpy(buf,msg.data);
dir=getdir(buf);
if(access(dir,F_OK)==-1)
{
printf("%s not exsit\n",dir);
}
else{
filefd=open(dir,O_RDWR);
read(filefd,msg.secondBuf,sizeof(msg.secondBuf));
close(filefd);
write(fd,&msg,sizeof(msg));
}
break;
case LLS:
system("ls");
break;
case LCD:
dir=getdir(msg.data);
chdir(dir);
break;
case QUIT:
strcpy(msg.data,"quit");
write(fd,&msg,sizeof(msg));
close(fd);
exit(-1);
}
return ret;
}
void handlerServerMessage(int c_fd,struct Msg msg)
{
int n_read;
struct Msg msgget;
int newfilefd;
n_read=read(c_fd,&msgget,sizeof(msgget));
if(n_read==0)
{
printf("server is out quit\n");
exit(-1);
}
else if(msgget.type==DOFILE)
{
char* p=getdir(msg.data);
newfilefd=open(p,O_RDWR|O_CREAT,06000);
write(newfilefd,msgget.data,strlen(msgget.data));
putchar('>');
fflush(stdout);
}
else
{
printf("----------------------------------------\n");
printf("\n%s\n",msgget.data);
printf("----------------------------------------\n");
putchar('>');
fflush(stdout);
}
}
int main(int argc, char **argv)
{
int c_fd;
struct sockaddr_in c_addr;//internet环境下套接字的地址形式
struct Msg msg;
memset(&c_addr,0,sizeof(struct sockaddr_in));
if(argc!=3)
{
printf("param is not good!\n");
exit(-1);
}
c_fd=socket(AF_INET,SOCK_STREAM,0);
if(c_fd==-1)
{
perror("socket");
exit(-1);
}
c_addr.sin_family=AF_INET;
c_addr.sin_port=htons(atoi(argv[2]));
inet_aton(argv[1],&c_addr.sin_addr);
//connect
if(connect(c_fd,(struct sockaddr*)&c_addr,sizeof(struct sockaddr))==-1)
{
perror("connect");
exit(-1);
}
printf("connect ...\n");
int mark=0;
while(1)
{
memset(msg.data,0,sizeof(msg.data));
if(mark==0) printf(">");
gets(msg.data);//获取用户输入
if(strlen(msg.data)==0)
{
if(mark==0)
{
printf(">");
}
continue;
}
mark=1;
int ret=cmdHandler(msg,c_fd);
if(ret>IFGO)
{
putchar('>');
fflush(stdout);
continue;
}
if(ret==-1)
{
printf("command net\n");
printf(">");
fflush(stdout);
continue;
}
handlerServerMessage(c_fd,msg);
}
return 0;
}
config.h
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define LS 0
#define GET 1
#define PWD 2
#define IFGO 3
#define LCD 4
#define LLS 5
#define CD 6
#define PUT 7
#define QUIT 8
#define DOFILE 9
struct Msg
{
int type;
char data[1024];
char secondBuf[128];
};