1.面向连接的套接字通信工作流程
(1)服务器先用socket函数来建立一个套接字,用这个套接字完成通信的监听
(2)用bind函数来绑定一个端口号和IP地址。因为本地计算机可能有多个IP,每一个IP有多个端口号,需要指定一个IP和端口进行监听
(3)服务器调用listen函数,使服务器的这个端口和IP出于监听状态,等待客户机的连接
(4)客户机用socket建立一个套接字
(5)客户机调用connect函数,通过远程IP和端口号连接远程计算机指定的端口
(6)服务器用accept函数来接收远程计算机的连接,建立起与客户端之间的通信
(7)建立连接以后,客户机用write函数向socket中写入数据。也可用read函数读取服务器发送来的数据
(8)服务器用read函数读取客户机发送来的数据,也可用write函数发送数据
(9)完成通信以后,用close函数关闭socket连接
2.绑定端口
绑定端口指的是将套接字与指定的端口相连。用socket建立起一个套接字以后,需要用bind函数在这个套接字上面绑定一个端口
(1)绑定端口函数bind
函数bind可以将一个端口绑定到一个已经建立好的socket上,bind函数定义如下:
int bind(int sockfd,struct sockaddr* my_addr,int addrlen);
参数列表中,sockfd是已经建立的socket编号。my_addr是一个指向struct sockaddr类型的struct sockaddr类型指针。sockaddr的定义如下:
struct sockaddr
{
unsigned short int sa_family;//调用socket时 的domain参数
char sa_data[14];//含有IP地址与端口的信息,最多使用14个字符长度
}
如果建立socket时使用的是AF_INET参数,则sockaddr_in结构体的定义如下:
struct sockaddr_in
{
unsigned short int sin_family;
unit16_t sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
}
结构体的成员addr也是一个结构体,定义如下:
struct in_addr
{
uint32_t s_addr;
}
参数addrlen是my_addr的长度。
函数可以把指定的IP和端口绑定到已经建立的socket上面。如果绑定成功,则返回0.失败则返回-1。函数可能发生下面的错误,可以用error捕获发生的错误
EBADF:参数sockfd不是一个合法的socket
EACCESS:权限不足
ENOTSOCK:参数sockfd是文件描述词,而不是socket。
3监听与连接
所谓监听,指的是socket的端口处于等待状态。如果有客户端请求连接,这个端口会接受这个连接。连接指的是客户端向服务端发送一个通信申请,服务端会响应这个请求。
(1)等待监听函数listen
服务端必须等待客户端的连接请求,listen函数用于实现监听等功能。listen函数定义如下所示:
int listen(int s,int backlog);
在参数列表中,s是已经建立的socket。backlog是能够处理的最大连接请求。如果超过这个数目,客户端会接收到ECONNREFUSED拒绝连接的错误。
listen并未真正接受连接,只是设置socket的状态为listen模式。真正接受客户端连接的是accept函数。通常情况下,listen函数会在socket,bind函数之后调用,然后才会调用accept函数
listen函数只使用SOCK_STREAM或SOCK_SEQPACKET的socket类型。如果socketwei AF_INET,则参数backlog最大值可设至128,则最多可同时接收128个客户端请求。如果调用成功,则函数的返回值为0,失败返回-1.函数可能发生错误,可以用errno来捕获发生的错误
(2)接受连接函数accept
服务器处于监听状态时,如果获得客户机的请求,会将这个请求放在等待队列中。当系统空闲时,将处理客户机的连接请求。使用accept函数建立连接,定义如下:
int accept(int s,struct sockaddr * addr,int* addrlen);
在参数列表中,s是处于监听状态的socket。addr是一个struct sockaddr类型的指针系统会把远程主机的信息(IP地址和端口)保存到这个结构体指针上。addrlen是sockaddr的内存长度。
当accept函数接受一个连接时,会返回一个新的socket文件句柄。以后的数据传输和读取就是通过这个新的socket文件句柄来处理的。原来参数中的socket可以继续使用。建立连接以后,远程主机的地址和端口信息将会保存到addr所指的结构体内。如果处理失败,返回-1.
(3)请求连接函数connect
所谓请求连接,指的是客户机需要向服务器发送信息前,需要发送一个连接请求。connect函数用来完成这项功能,connect函数定义如下所示:
int connect(int sockfd,struct sockaddr* serv_addr,int addrlen);
在参数列表中,sockfd是已经建立的socket。serv_addr是一个结构体指针,指向一个struct sockaddr结构体。这个结构体存储着远程服务器的IP和端口信息。addrlen是struct addrlen结构体的内存长度。connect函数会将本地的socket连接到serv_addr所指定的服务器IP与端口。如果连接成功,返回值为0,连接失败则返回-1。函数可能发生错误,可以用error来捕获发生的错误。
4.数据的发送与接收
建立套接字并完成网络连接以后,可以把信息传送到远程主机上,这个过程就是信息的发送。远程主机发送来的信息,本地主机需要进行接收处理。
(1)数据接收函数recv
函数recv可以接收远程主机发送来的数据,并把这些数据保存到一个数组中,recv函数定义如下:
int recv(int s, void *buf,int len,unsigned int flags)
在参数列表中,s是已经建立的socket。buf是一个指针,指向一个数组。接收到的数据会保存到这个数组上。len是数组的长度。flags一般设置为0,其他可能的赋值与含义如下所示:
MSG_OOB:接收以out-of-band送出的数据
MSG_PEEK:返回来的数据并不会在系统内删除,如果再调用recv时会返回相同的数据内容
MSG_WAITALL:强迫接收到len大小的数据后才能返回,除非有错误或信号产生
MSG_NOSIGNAL:此操作被SIGPIPE信号中断
recv如果接收到数据,会把这些数据保存在buf指针指向的内存中,然后返回接收到字符的个数。如果发生错误则会返回-1.函数可能发生下面这些错误,可以用errno来捕获错误:
EINTR:函数被信号中断
EINVAL:传入的参数不正确
ENOMEM:没有足够内存
EBADF:错误的‘socket文件描述符
(2)信息发送函数send
用connect函数连接到远程计算机以后,可以用send函数将信息发送到对方的计算机。send函数定义如下:
int send(int s,const void *msg,int len,unsigned int flags);
在参数列表中国,s是已经建立的socket。msg是需要发送数据的指针。len是需要发送数据的长度。参数flags一般设置为0,可能的设置与赋值如下:
MSG_OOB:传送的数据以out-of-band的方式送出
MSG_DONTROUTE:取消路由表查询
MSG_NOSIGNAL:此传输不可被SIGPIPE信号中断
如果数据发送成功,函数会返回已经传送的字符个数。否则会返回-1.函数可能发生错误,可以用errno来捕获函数的错误
(3)数据传输函数write与read
socket连接以后,向这个socket写入数据表示向远程主机传送数据,从socket中读取数据,相当于接收远程主机传送过来的数据。这两个函数的定义如下:
ssize_t write(int fd,const void *buf,size_t count);
ssize_t read(int fd,void * buf,size_t count);
在参数列表中,fd是已经建立的socket。buf是指向一段内存的指针,count表示buf指向内存的长度。read函数读取字节时,会把读取的内容保存到buf指向的内存中,然后返回读取到字节的个数。用write函数传输数据时,会把buf指针指向的内存中的数据发送到socket连接的远程主机,然后返回实际发送字节的个数。
5.函数补充
(1)perror
函数定义:void perror(const char *s);
perror函数用来将上一个函数发生错误的原因输出到标准设备(stderr)。参数s所指的字符串会先打印出,后面再加上错误原因字符串。此错误原因依照全局变量errno的值来决定要输出的字符串。在库函数中有个errno变量,每个errno值对应着以字符串表示的错误类型。当调用某些函数出错时,该函数已经重新设置了errno的值。perror函数只是将你输入的一些信息和现在的errno所对应的错误一起输出。
(2)exit
函数定义:void exit(int status ) ;
exit()函数用于正常终止一个进程,参数status&0377作为终止状态,返回给父进程。
调用exit函数时,通过函数atexit或on_exit注册的所有函数(终止处理程序)都会被调用,且调用的顺序刚好与注册时的顺序相反。(在这些终止处理程序中有可能是为了在程序终止时执行一个额外的处理,新注册的函数会添加到注册队列的头部,然后等待被调用。)如果这些终止处理程序没有返回退出(比如:_exit函数或kill a signal),那么剩余的终止程序将不会被调用。exit函数也将被抛弃终止。如果同一个函数被注册多次,则也会被调用多次。
调用函数exit后,所有的IO流都会被关闭,由tmpfile创建的文件也会被移除。
在c标准中设置了两个常量,EXIT_SUCCESS和EXIT_FAILURE,用来作为exit函数的参数,分别指出进程是正常退出还是异常终止。
exit函数没有返回值
函数exit使用了一个不受保护的全局变量,所以他不是安全的线程。
如果终止处理函数调用了exit或longjmp函数,那发生的后果将是无法预知的。注意,调用execve函数将移除所有终止处理程序。
调用exit函数后,退出进程,终止状态必须传递给父进程。这时,会发生三种情况:如果父进程设置了SA_NOCLDWAIT标志位(SA_NOCLDWAIT:使父进程在他的子进程退出时不会收到SIGCHLD信号,这时子进程退出也不会成为僵尸进程(僵尸进程是指一个已经终止,但其父进程尚未对其进行善后处理获取终止进程的有关信息的进程))。或者将信号SIGCHLD交给SIG_IGN(当一个进程正常或异常终止时,内核就像父进程发送SIGCHLD信号,如果父进程不处理,交给SIG_ING,则相当于父进程忽略子进程的生死)子进程的终止状态会被抛弃。如果父进程正在等待取得子进程结束的终止状态,在这两种情况下,子进程将会马上终止,释放资源。如果一个已经终止,但父进程尚未对其进行善后处理的进程,被称为僵尸进程(进程已经终止,但是还没释放所占用的资源)。所以父进程可以通过调用wait函数来等待处理退出的子进程。
如果系统支持信号机制,可以通过信号传递给父进程,但如果父进程设置了SA_NOCLDWAIT标志位,父进程将无法收到信号
如果进程是回话首进程且控制着一个控制终端,该进程终止后,该回话组的所有进程都会收到一个SIGHUP信号。同时终端也将会被释放,这些进程或=终端可以加入到别的进程组
如果退出进程是的该组其他进程成为孤儿进程,这些孤儿进程将会终止,同时会接收到由信号处理程序SIGCONT发来的SIGHUP信号
(3)bzero
bzero函数会将内存块(字符串)的前n个字节清零,其原型为:void bzero(void *s,int n)
其中,s为内存块(字符串)指针,n为需要清零的字节数
bzero函数会将参数s所指的内存块区域前n个字节,全部设置为零值
实际上,bzero函数等价于memset((void *)s,0,size_tn),用来将内存块的前n个字节清零
(4)FD_ZERO
函数定义:FD_ZERO(fd_set*fdset);指将指定的文件描述符集清空,在对文件描述符集合进行设置前,必须对其进行初始化,如果不清空,由于在系统分配内存空间后,通常并不做清空处理,所以结果是不可知的
(5)FD_SET
函数定义:FD_SET(fd_set*fdset)用于在文件描述集合中增加一个新的文件描述符
(6)select
函数定义:int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout );
参数maxfd是需要监视的最大文件描述符值加一;rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合。struct timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0
select函数用于在非阻塞中,当一个套接字或一组套接字有信号通知你时,系统提供select函数来实现多路复用输入/输出模型
(7)FD_ISSET
FD_ISSET(int fd,fd_set*fdset);用于测试指定的描述符是否在该集合中
(8)fd_set:是一组文件描述符的集合,他用一位来表示一个fd
(9)文件描述符
文件描述符在形式上是一个非负整数。实际上,他是一个索引值,指向内核。当程序打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。
文件描述符是用无符号整数表示的句柄,进程使用它来标识打开的文件。文件描述符与包括相关信息(如文件的打开模式,文件的位置类型,文件的初始类型等)的文件对象相关联,这些信息被称作文件的上下文。
在linux系统中打开文件就会获得文件描述符,他是一个很小的整数。每个进程在PCB(Process Control Block)中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向以打开文件的指针。