运行在不同机器上的进程彼此通过向套接字发送报文来进行通信。每个进程好比是一座房子,进程的套接字就好比是一个门。套接字是应用进程和TCP之间的门,应用程序开发者可以控制套接字的应用层那一侧所有的东西,但是不能控制运输层那一侧。
服务器为了能对客户机程序发起连接作出响应,应满足:
第一、服务器程序不能处于休眠状态;
第二、服务器程序必须有某种套接字。
socket通信流程:
1、服务器根据地址类型(ipv4,ipv6)、socket类型、协议创建socket
2、服务器为socket绑定ip地址和端口号
3、服务器socket监听端口号请求,随时准备接收客户端发来的连接,这时候服务器的socket并没有被打开
4、客户端创建socket
5、客户端打开socket,根据服务器ip地址和端口号试图连接服务器socket
6、服务器socket接收到客户端socket请求,被动打开,开始接收客户端请求,直到客户端返回连接信息。这时候socket进入阻塞状态,所谓阻塞即accept()方法一直到客户端返回连接信息后才返回,开始接收下一个客户端谅解请求
7、客户端连接成功,向服务器发送连接状态信息
8、服务器accept方法返回,连接成功
9、客户端向socket写入信息
10、服务器读取信息
11、客户端关闭
12、服务器端关闭
1、创建一个套接字---socket()
socket()函数用于根据指定的地址族,数据类型和协议来分配一个套接字的描述字及其所用的资源。
#include <sys/socket.h>
int socket (int domain, int type, int protocol);
domain:表示套接字要使用的协议族,协议族在“linux/socket.h”里有详细的定义,常用的协议族:
<1>:AF_UNIX(本机通信)
<2>:AF_INET(TCP/IP-IPv4)DOS、Windows中仅支持AF_INET
<3>:AF_INET6(TCP/IP-IPv6)
type:套接字类型,常用类型:
<1>:SOCK_STREAM(TCP流:提供了面向连接、可靠数据传输服务,数据无差错。无重复地发送,且按发送顺序接收。内设流量控制,避免数据流超限;数据被看做是字节流,无长度限制,文件传送协议(FTP)即使用流式套接字)
<2>:SOCK_DGRAM(UDP数据报:提供了无连接服务。数据报以独立包的形式被发送,不提供无错保证,数据可能会丢失或重复,并且接收混乱。网络文件系统(NFS)使用数据报式套接字)
<3>:SOCK_RAW(原始套接字:该接口允许对较低层协议,如IP、ICMP直接访问。常用于检验新的协议实现或访问现有服务中配置的新设备)
protocol:如果调用者不希望特别指定使用的协议,一般设置为0,使用默认的连接方式。
socket是一个函数,那么他也有返回值,当套接字成功创建时,返回套接字,失败返回“-1”;错误代码则写入“ERRNO”中;
创建套接字:
#include <sys/type.h>
#include <sys/socket.h>
#include <linux/socket.h>
int soc_fd_tcp;
int soc_fd_udp;
soc_fd_tcp = socket(AF_INET, SOCK_STREAM,0);
soc_fd_udp = socket(AF_INET, SOCK_DGRAM, 0);
if(soc_fd_tcp < 0)
{
perror("TCP SOCKET ERROR\n");
exit(-1);
}
if(soc_fd_udp < 0)
{
perror("UDP SOCKET ERROR\n");
exit(-1);
}
不同的应用程序对应不同的socket,那么socket里到底是什么?
答:socket套接字地址!套接字地址是一个结构体,以TCP传输协议作为例子,套接字地址这个数据结构里面包含了:地址类型、端口号、IP地址、填充字节这4中数据,原型为:
#include <netinet/in.h>
struct sockaddr_in
{
unsigned short sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
其中:
sin_family: 表示地址类型,对于基于TCP/IP传输协议的通信,该值只能是AF_INET;
sin_port:表示端口号,用来辨别本地通讯进程,由于每个进程都有自己的端口号,因此在通讯之前必须要分配一个没有被访问的端口号。
sin_add:表示32位的IP地址;
sin_zero:表示填充字节,一般情况下该值为0;
socket数据的赋值:
struct sockaddr_in sin;//设置一个sin套接字地址
sin.sin_family = AF_INET;//它基于TCP/IP协议,因此,sin_family的值为AF_INET
sin.sin_port = htons(80); //htons是端口函数,表示端口号为80
sin.sin_addr.s_addr = inet_addr("202.96.134.133");//sin_addr是一个结构体
memset(sin.sin_zero, 0, sizeof(sin.sin_zero));//给数组sin_zero清零
sin_addr结构体原型:
struct in_addr
{
unsigned long s_addr;
};
2、指定本地地址---bind()函数:
将本地地址与一套接字捆绑。当socket()创建套接字后,它便存在于一个名字空间(地址族)中,bind()函数通过给一个未命名的套接字分配一个本地名字,来为套接字建立本地捆绑(主机地址/端口号)。
例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋值给socket。
int bin(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数的三个参数分别为:
sockfd:即socket描述字,它是通过socket()函数创建的,唯一表示一个socket。bind()函数就是给这个描述字绑定一个名字。
addr:一个const struct sockaddr* 指针,指向要绑定给sockfd的协议地址。这个地址结构,根据地址创建socket时的地址协议族的不同而不同。如IPv4:
struct sockaddr_in
{
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr
{
uint32_t s_addr; /* address in network byte order */
};
ipv6对应的是:
struct sockaddr_in6
{
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
};
struct in6_addr
{
unsigned char s6_addr[16]; /* IPv6 address */
};
addrlen:表示对应的地址长度。
通常服务器在启动的时候都会绑定一个众所周知的地址(IP地址+端口号),用于提供服务,客户就可以通过它来连接服务器;而客户端就不用指定,系统会自动分配一个端口号和自身IP地址组合。这就是为什么服务器端会在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
3、监听和请求连接---listen()、connect()函数:
作为服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket(),作为客户端,这时应该调用connect(),发出连接请求,服务器端就会接收到这个请求
int listen(int sockfd, int backlog);
sockfd表示:本地已建立、尚未连接的套接字号,服务器会在他的上面接收请求。backlog表示请求连接队列的最大长度,就是告诉套接字在忙于处理上一个请求时还可以接受多少个请求进入,换句话来说,这决定挂起连接的队列的最大大小,用于限制排队请求的个数,目前允许的最大值是5.如果没有发生错误,listen()返回0,否则,返回:SOCKET_ERROR。
listen()在执行调用过程中可为没有调用过的bind()的套接字sockfd完成所必须的连接,并建立长度为backlog的请求连接队列。
调用listen()是服务器接收一个连接请求的四个步骤中的第三步。它在调用socket()分配一个流套接字,且调用bind()给sockfd赋值于一个名字之后,调用!而且要在accept()之前调用。
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
第一个参数即为:客户端的socket描述字;
第二个参数为服务器的socket的地址;
第三个参数为socket地址的长度,客户端通过调用connect函数与服务器来建立TCP连接。
4、建立套接字连接---accept()函数:
accept()用于面向连接服务器。
TCP服务器端一次调用socket()、bind()、listen()之后,就会监听指定的socket()地址了,TCP客户端依次调用socket()、connect()之后就向TCP服务器发送一个连接请求,TCP服务器监听到这个请求之后,就会调用accept()函数来接收请求,这样连接便建立好了,之后就可以网络I/O操作了,即类同于普通文件的读写I/O操作。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept函数:
第一个参数为服务器的socket描述字;
第二个参数为指向struct sockaddr* 的指针,用于返回客户端的协议地址;
第三个参数为协议地址的长度。如果accept成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。
accept(),参数addr和addrlen存放客户方的地址信息。调用前,参数addr 指向一个初始值为空的地址结构,而addrlen 的初始值为0;调用accept()后,服务器等待从编号为sockfd的套接字上接受客户连接请求,而连接请求是由客户方的connect()调用发出的。当有连接请求到达时,accept()调用将请求连接队列上的第一个客户方套接字地址及长度放入addr
和addrlen,并创建一个与sockfd有相同特性的新套接字号。新的套接字可用于处理服务器并发请求。
注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为:监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常仅仅只创建一个监听socket描述字,它在服务器的生命周期内一直存在。内核为每个服务器进程接收的客户连接创建一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的连接socket描述字就被关闭。
5、数据传输---send()与recv()
int send(int sockfd, const void *buf, size_t len, int flags);
不论客户端还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。客户端一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程序发送应答。
send函数:
第一个参数是指定发送端套接字描述字;
第二个参数是指明一个存放应用程序要发送数据的缓冲区,
第三个参数指明要发送的数据的字节数,
第四个参数一般置为0;
在这里描述socket的send函数的执行流程:
当调用send函数时,send先比较待发送数据的长度len和套接字sockfd的发送缓冲长度,如果len大于sockfd的发送缓冲区长度,该函数返回SOCKET_ERROR;如果len小于或者等于sockfd的发送缓冲区长度,那么send先检查协议,是否正在发送sockfd的发送缓冲区的数据,如果是,就等待协议把数据发送完,如果协议还没有开始发送sockfd的发送缓冲区的数据或者sockfd发送缓冲区没有数据,那么send就比较sockfd的发送缓冲区的剩余空间和len,如果len大于剩余空间,send就一直等待协议把sockfd的发送缓冲区的数据发送完,如果len小于剩余空间大小,send就仅仅把buf中的数据copy到剩余空间里(注意:并不是send把sockfd的发送缓冲中的数据传送到连接的另一端,而是协议传的,send仅仅是把buf中的数据copy到sockfd的发送缓冲区的剩余空间里)。如果send函数copy数据成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCK_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR.
要注意:send函数把buf中的数据成功copy到sockfd的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个Socket函数就会返回SOCKET_ERROR。(每一个除send外的Socket函数在执 行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该Socket函数就返回
SOCKET_ERROR);
Send函数的返回值有三类:
(1)返回值=0:
(2)返回值<0:发送失败,错误原因存于全局变量errno中
(3)返回值>0:表示发送的字节数(实际上是拷贝到发送缓冲中的字节数)
int recv(int sockfd, void *buf, size_t len, int flags);
不论是客户端还是服务器端应用程序都用recv函数从TCP连接的另一端接收数据。
recv函数的:
第一个参数指定接收端套接字描述字;
第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
第三个参数指明buf的长度;
第四个参数一般设置为0;
现在描述socket的recv函数的执行流程:当应用程序调用recv函数时,recv先等待sockfd发送缓冲区中的数据被协议传送完毕,如果协议再传送sockfd的发送缓冲区中的数据时出现了网络错误,那么recv函数返回SOCKET_ERROR,如果数据缓冲区中没有数据或者数据被协议成功发送完后,recv函数先检查套接字sockfd的接收缓冲区,如果sockfd的接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕,当协议把数据接收完毕,recv函数就把sockfd的接收缓冲中的数据copy到buf中(注意:协议接收到数据可能大于buf的长度,所以这种情况下要多次调用recv函数才能把接收缓冲中的数据copy完,recv函数仅仅是copy函数,真正接收数据是协议来完成的。)recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
recv函数的返回值类型:
(1)成功执行时,返回接收到的字节数。
(2)若另一端已关闭连接则返回0,这种关闭是对方主动且正常的关闭
(3)失败返回-1
6、关闭套接字---close()
close()函数:关闭套接字s,并释放分配给该套接字的资源;如果s涉及一个打开的TCP连接,则该连接被释放。
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。
#include <unistd.h>
int close(int fd);
close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。