服务器端:
第一步,调用socket函数创建套接字
第二步,调用bind函数分配IP地址和端口号
第三步,调用listen函数转为可接收请求状态
第四步,调用accept函数受理连接请求
#include<sys/socket.h>
int socket(int domain, int type, int protocol);//成功时返回文件描述符,失败返回-1
一、domain: 套接字中使用的协议族信息
PF_INET: IPv4互联网协议族
PF_INET6: IPv6互联网协议族
PF_LOCAL: 本地通信的UNIX协议族
PF_PACKET: 底层套接字的协议族
PF_IPX: IPX Novell协议族
另外,套接字中实际采用的最终协议信息是通过socket函数中的第三个参数传递的。在指定的协议族范围内通过第一个参数决定第三个参数。
二、套接字类型
(1)面向连接的套接字(SOCK_STREAM)
1.传输过程中数据不会消失
2.按序传输数据
3.传输的数据不存在数据边界(read和write的调用次数并无太大意义)
注意:套接字连接必须一一对应。可靠的、按序传递的、基于字节的面向连接的数据传输方式的套接字。
(2)面向消息的套接字(SOCK_DGRAM)
1.强调快速传输而非传输顺序
2.传输的数据可能丢失也可能损毁
3.传输的数据有数据边界(接收数据的次数和传输次数相同)
4.限制每次传输的数据大小
注意:不可靠的、不按序传递的、以数据的高速传输为目的的套接字。
TCP套接字的数据收发无边界,服务器端即使调用一次write函数传输40字节的数据,客户端也有可能通过4次read函数调用每次读取10字节。那么读取10字节后,剩下的30字节在哪等候呢?事实上,write函数调用后并非立即传输数据,read函数调用后也并非马上接收数据。write函数调用瞬间,数据将移至输出缓冲;read函数调用瞬间,从输入缓冲读取数据。
缓冲特性:
1. I/O缓冲在每个TCP套接字中单独存在
2. I/O缓冲在创建套接字时自动生成
3. 即使关闭套接字也会继续传递输出缓冲中遗留的数据
4. 关闭套接字将丢失输入缓冲中的数据
write函数在数据移到输出缓冲时返回。TCP保证对输出缓冲数据的传输。
流控制是区分UDP和TCP的最重要的标志。
UDP套接字:只需一个UDP套接字就能和多台主机通信。
UDP服务器端/客户端不想TCP那样在连接状态下交换数据,因此与TCP不同,无需经过连接过程。也就是说,不必调用TCP连接过程中调用的listen函数和accept函数。UDP中只有创建套接字的过程和数据交换过程。
将初始化的地址信息分配给套接字:
#include<sys/socket.h>
int bind(int sockfd, struct sockaddr* myaddr, socklen_t addrlen);//成功返回0,失败返回-1
如果此函数调用成功,则将第二个参数指定的地址信息分配给第一个参数中的相应套接字。
struct sockaddr_in
{
sa_family_t sin_family; //地址族
uint16_t sin_port; //16位TCP/UDP端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用
}
struct in_addr
{
in_addr_t s_addr; //32位IPv4地址
}
一、sa_family_t地址族:
AF_INET: IPv4网络协议中使用的地址族
AF_INET6: IPv6网络协议中使用的地址族
AF_LOCAL: 本地通信中采用的UNIX协议的地址族
#include<sys/socket.h>
int listen(int sockfd, int backlog);//成功返回0,失败返回-1
backlog:连接请求等待队列的长度,若为5,则队列长度为5,表示最多使5个连接请求进入队列。
调用listen函数后,若有新的连接请求,则应按序受理:
#include<sys/socket.h>
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);成功时返回文件描述符,失败返回-1
addr:保存发起连接请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充客户端地址信息。
addrlen:第二个参数addr结构体的长度,但是存有长度的变量地址。函数调用完成后,该变量即被填入客户端地址长度。
客户端:
第一步,调用socket函数创建套接字
第二步,调用connect函数向服务器端发送连接请求
#include<sys/socket.h>
int connect(int sock , struct sockaddr* servaddr , socklen_t addrlen);//成功返回0,失败返回-1
sock:客户端套接字文件描述符
servaddr:保存目标服务器端地址信息的变量地址值
addrlen:以字节为单位传递已传递给第二个结构体参数servaddr的地址变量长度
客户端的IP地址和端口在调用connect函数时自动分配,无需调用标记的bind函数进行分配。
connect函数其实是服务器端把连接请求信息记录到等待队列,因此connect函数返回后并不立即进行数据交换。
将字符串形式的IP地址转换成32位整数型数据:
#include <arpa/inet.h>
in_addr_t inet_addr(const char* string);//成功返回32位大端序整数型值,失败返回INADDR_NONE。
inet_aton函数与inet_addr函数在功能上完全相同,也将字符串形式IP地址转换为32位网络字节序整数并返回。只不过该函数利用了in_addr结构体,且其使用频率更高。
#include<arpa/inet.h>
int inet_aton(const char* string , struct in_addr* addr);//成功返回1,失败返回0;
//string:含有需转换的IP地址信息的字符串地址值
//addr:将保存转换结果的in_addr结构体变量的地址值
inet_ntoa函数正好与inet_aton函数相反。此函数可以把网络字节序整数型IP地址转换成我们熟悉的字符串形式。
#include<arpa/inet.h>
char* inet_ntoa(struct in_addr adr);//成功返回转换的字符串地址值,失败返回-1
struct sockaddr_in addr;
char* serv_port = "9190";
memset(&addr,0,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(atoi(serv_port));
memset函数将每个字节初始化为同一值:第一个参数为结构体变量addr的地址值,即初始化对象为addr;第二个参数为0,因此初始化为0;最后一个参数中传入addr的长度,因此addr的所有字节均初始化为0。
atoi函数把字符串类型的值转换为整数型。
利用常数INADDR_ANY分配服务器端的IP地址,可以自动获取运行服务器端的计算机IP地址。
基于UDP的数据I/O函数
发送:
#include<sys/socket.h>
ssizez_t sendto(int sock, void* buff, size_t nbytes, int flags, struct sockaddr* to, socklen_t addrlen);//成功返回传输字节数,失败返回-1。
//sock:用于传输数据的UDP套接字文件描述符
//buff:保存待传输数据的缓冲地址值
//nbytes:待传输的数据长度,以字节为单位
//flags:可选项参数,若没有则传递0
//to:存有目标地址信息的sockaddr结构体变量的地址值
//addrlen:传递给参数to的地址值结构体变量的长度
接收:
#include<sys/socket.h>
ssizez_t recvfrom(int sock, void* buff, size_t nbytes, int flags, struct sockaddr* from, socklen_t* addrlen);//成功返回传输字节数,失败返回-1。
//sock:用于接收数据的UDP套接字文件描述符
//buff:保存接收数据的缓冲地址值
//nbytes:可接收的最大字节数,故无法超过参数buff所指的缓冲大小
//flags:可选项参数,若没有则传递0
//from:存有发送端地址信息的sockaddr结构体变量的地址值
//addrlen:保存参数from的结构体变量长度的变量地址值
UDP程序中,调用sendto函数传输数据前应完成对套接字的地址分配工作,因此调用bind函数。即bind函数不分TCP和UDP。另外,如果调用sendto函数时发现尚未分配地址信息,则在首次调用sendto函数时给相应套接字自动分配IP和端口。而且此时分配的地址一直保留到程序结束为止,因此也可用来与其他UDP套接字进行数据交换。综上所述,调用sendto函数时,自动分配IP和端口号,IP用主机IP,端口号选尚未使用的任意端口号。因此,UDP客户端通常无需额外的地址分配过程。
2两台主机建立了套接字连接,每个主机就会拥有单独的输入流和输出流。close函数将同时断开这两个流。shutdown函数用来关闭其中一个流:
#include<sys/socket.h>
int shutdown(int sock , int howto);//成功返回0,失败返回-1
//sock:需要断开的套接字文件描述符
//howto:传递断开方式信息
//SHUT_RD:断开输入流(套接字无法接收数据,即使输入缓冲收到数据也会抹去,而且无法调用输入相关函数)
//SHUT_WR:断开输出流(无法传输数据,但如果输出缓冲中还有未传输的数据,则将传递至目标主机)
//SHUT_RDWR:同时断开I/O流
半关闭的作用:向客户端传递EOF表示文件传输结束,防止客户端无休止调用输入函数导致的程序阻塞。
利用域名获取IP地址:通过传递字符串格式的域名获取IP地址
#include<netdb.h>
struct hostent* gethostbyname(const char* hostname);//成功返回hostent结构体地址,失败返回NULL指针
struct hostent
{
char* h_name; //官方域名
char** h_aliases; //可以通过多个域名访问同一主页
int h_addrtype; //可以通过此变量获取保存在h_addr_list的IP地址的地址族信息
int h_length; //保存IP地址长度
char** h_addr_list; //以整数形式保存域名对应的IP地址
}
//域名转IP时只需关注 h_addr_list 。
利用IP地址获取域名:
#include<netdb.h>
struct hostent* gethostbyaddr(const char* addr, socklen_t len, int family);//成功返回hostent结构体变量地址值,失败返回NULL指针
//addr:含有IP地址信息的in_addr结构体指针
//len:向第一个参数传递的地址信息的字节数,IPv4为4,IPv6为16
//family:传递地址族信息,IPv4为AF_INET,IPv6为AF_INET6
套接字可选项的读取和设置通过两个函数完成:
读取:
#include <sys/socket.h>
int getsockopt(int sock, int level, int optname, void* optval, socklen_t* optlen);//成功返回0,失败返回-1
//sock:用于查看选项套接字文件描述符
//level:要查看的可选项的协议层
//optname:要查看的可选项名
//optval:保存要查看结果的缓冲地址值
//optlen:向第四个参数optval传递的缓冲大小,调用函数后,该变量中保存通过第四个参数返回的可选项信息的字节数。
设置:
#include <sys/socket.h>
int setsockopt(int sock, int level, int optname, const void* optval, socklen_t optlen);//成功返回0,失败返回-1
//sock:用于更改可选项的套接字文件描述符
//level:要更改的可选项协议层
//optname:要更改的可选项名
//optval:保存要更改的选项信息的缓冲地址值
//optlen:向第四个参数optval传递的可选项信息的字节数
I/O缓冲可选项:
SO_RCVBUF是输入缓冲(write)大小相关可选项
SO_SNDBUF是输出缓冲(read)大小相关可选项
这两个可选项既可以读取当前I/O缓冲大小,也可以进行修改。
后台处理是指将控制台窗口中的指令放到后台运行的方式。如果以如下方式运行程序,则程序将在后台运行(&将触发后台处理):
./hello_world &
如果采用这种方式运行程序,即可在同一控制台输入下列命令,无需另外打开新控制台:
ps au