文章目录
一、使用 TCP 的流程图
TCP 通信的基本步骤:
TCP 通信的基本步骤中服务器端的情况。
1.1 头文件包含
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
1.2 socket 函数
生成一个套接口描述符。
int socket(int domain, int type, int protocol);
参数 domain → {AF_INET:IPv4 网络协议 AF_INET6:IPv6 网络协议}
参数 type → {tcp:SOCK_STREAM udp:SOCK_DGRAM}
参数 protocol → 指定 Socket 所使用的传输协议编号,通常为 0 。
返回值:成功则返回套接口描述符,失败返回 -1 。
示例:
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1)
{
perror("socket");
exit(-1);
}
1.3 bind 函数
用来绑定一个端口号和 IP 地址,使套接口与指定的端口号和 IP 地址相关联。
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
参数 sockfd → 前面 socket 的返回值。
参数 my_addr → 结构体指针变量。
参数 addrlen → sockaddr 的结构体长度。通常计算 sizeof(struct sockadd);
返回值:成功则返回 0,失败返回 -1。
对于不同的 socket domain 定义了一个通用的数据结构:
struct sockaddr //不常用
{
unsigned short int sa_family; //调用 socket() 时的 domain 参数,即 AF_INET 值
char sa_data[14]; //最多使用14个字符串长度
};
此 sockaddr 结构会因使用不同的 socket domain 而有不同的结构定义,如使用 AF_INET domain,其 sockaddr 结构定义为:
struct aockaddr_in //常用
{
unsigned short int sin_family; //即为 sa_family → AF_INET
uint16_t sin_port; //为使用的 port 编号
struct in_addr sin_addr; //为 IP 地址
unsigned char sin_zero[8]; //未使用
};
struct in_addr
{
uint32_t s_addr;
};
示例:
struct sockaddr_in my_addr; //定义结构体变量
memset(&my_addr, 0, sizeof(struct sockaddr)); //将结构体清空
//或 bzero(&my_addr, sizeof(struct sockaddr));
my_addr.sin_family = AF_INET; //表示采用IPv4网络协议
my_addr.sin_port = htons(8888); //表示端口号为8888,通常是一个大于1024的值
//htons()用来将参数指定的16位hostshort转换成网络字符顺序
my_addr.sin_addr.s_addr = inet_addr("192.168.0.101");
//inet_addr() 用来将IP地址字符串转换成网络所使用的二进制数字,如果INADDR_ANY,表示服务器自动填充本机IP地址
if(bind(sfd, (struct sockaddr *)&my_addr, sizeof(struct socket)) == -1)
{
perror("bind");
close(sfd);
exit(0);
}
注意:通过将 my_addr.sin_port 置为 0,函数会自动为你选择一个未占用的端口号来使用。
同样,通过将 my_addr.sin_addr.a_addr 置为 INADDR_ANY,系统会自动填入本机 IP 地址。
1.4 listen 函数
使服务器的这个端口和 IP 处于监听状态,等待网络中某一客户机的连接请求。如果客户端有连接请求,端口就会接受这个连接。
int listen(int sockfd, int backlog);
参数 sockfd 为前面 Socket 的返回值。
参数 backlog 指定同时能处理的最大连接要求,通常为 10 或者 5 。最大值可设至 128 。
返回值:成功则返回 0,失败返回 -1 。
示例:
if(listen(sfd, 10) == -1)
{
perror("listen");
close(sfd);
exit(-1);
}
1.5 accept 函数
接受远程计算机的连接请求,建立起与客户机之间的通信连接。
服务器处于监听状态时,如果某时刻获得客户机的连接请求,此时并不是立即处理这个请求,而是将这个请求放在等待队列中,当系统空闲时再处理客户机的连接请求。
当 accept 函数接受一个连接时,会返回一个新的 Socket 标识符,以后的数据传输和读取就要通过这个新的 Socket 编号来处理,原来参数中的 Socket 也可以继续使用,继续监听其他客户机的连接请求。
类似于移动营业厅,如果有客户打电话给 10086,此时服务器就会请求连接,处理一些事务之后,就通知一个话务员接听客户的电话,后面的所有操作,此时已与服务器没有关系,而是话务员跟客户的交流。
对应过来,客户请求连接服务器,服务器先做了一些绑定和监听等操作之后,如果允许连接,则调用 accept 函数产生一个新的套接字,然后用这个新的套接字跟客户进行数据收发。
也就是说,服务器跟一个客户端连接成功,会有两个套接字。
int accept(int s, struct sockaddr *addr, int *addrlen);
参数 s 为前面 Socket 的返回值,即 sfd
参数 addr 为结构体变量,和 bind 的结构体是同种类型的,系统会把远程主机的信息(远程主机的地址和端口号信息)保存到这个指针所指的结构体中。
参数 addrlen 表示结构体的长度,为整型指针。
返回值:成功则返回新的 Socket 处理代码 cfd,失败返回 -1。
示例:
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(struct sockaddr));
int addrlen = sizeof(struct sockaddr);
int cfd = accept(sfd, (struct sockaddr *)&clientaddr, &addrlen);
if(cfd == -1)
{
perror("accept");
close(sfd);
exit(-1);
}
printf("%s %d success connect\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
1.6 recv 函数
用新的套接字来接收远端主机传来的数据,并把数据存到由参数 buf 指向的内存空间。
int recv(int sockfd, void *buf, int len, unsigned int flags);
参数 sockfd 为前面 accept 的返回值,即 cfd,也就是新的套接字。
参数 buf 表示缓冲区。
参数 len 表示缓冲区的长度。
参数 flags 通常为 0。
返回值:成功则返回实际接收到的字符数,可能会少于你所指定的接收长度;失败返回 -1 。
示例:
char buf[512] = {0};
if(recv(cfd, buf, sizeof(buf), 0) == -1)
{
perror(recv);
close(cfd);
close(sfd);
exit(-1);
}
puts(buf);
1.7 send 函数
用新的套接字发送数据给指定的远端主机。
int send(int s, const void *msg, int len, unsigned int flags);
参数 s 为前面 accept 的返回值,即 cfd。
参数 msg 一般为常量字符串。
参数 len 表示长度。
参数 flags 通常为 0 。
返回值:成功则返回实际传送出去的字符数,可能会少于你所指定的发送长度,失败返回 -1 。
示例:
if(send(cfd, "hello", 6, 0) == -1)
{
perror("send");
close(cfd);
close(sfd);
exit(-1);
}
1.8 close 函数
当使用完文件后若已不再需要则可使用 close() 关闭该文件,并且 close() 会让数据写回磁盘,并释放该文件所占用的资源。
int close(int fd);
参数 fd 为前面的 sfd,cfd 。
返回值:若文件顺利关闭则返回 0,发生错误则返回 -1 。
示例:
close(cfd);
close(sfd);
客户端
1.9 connect 函数
用来请求连接远程服务器,将参数 sockfd 的 Socket 连至参数 serv_addr 指定的服务器 IP 和端口号上去。
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
参数 sockfd 为前面 Socket 的返回值,即 sfd。
参数 serv_addr 为结构体指针变量,存储着远程服务器的 IP 与端口号信息。
参数 addrlen 表示结构体变量的长度。
返回值:成功则返回 0,失败返回 -1 。
示例:
struct sockaddr_in seraddr;
memset(&seraddr, 0, sizeof(struct sockaddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(8888);
seraddr.sin_sddr.s_addr = inet_addr("192.168.0.101");
if(connect(sfd, (struct sockaddr *)&seraddr, sizeof(struct sockaddr)) == -1)
{
perror("connect");
close(sfd);
exit(-1);
}
二、使用 UDP 的流程图
UDP 通信流程图:
2.1 sendto 函数
原型为:
int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);
该函数比 send 函数多两个参数;
参数 to 表示目的机的 IP 地址和端口号信息;
参数 tolen 常常被赋值为 sizeof(struct sockaddr);
返回值:成功返回实际发送的数据字节长度,出现发送错误时返回 -1 。
2.2 recvfrom 函数
原型为:
int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);
参数 from 表示连接机的 IP 地址和端口号信息;
参数 fromlen 常置为 sizeof(struct sockaddr);函数返回时,fromlen 包含实际存入 from 中的数据字节数;
返回值:成功返回接收到的字节数,出现错误时返回 -1,并置相应的 errno 。
三、设置套接口的选项 setsockopt 的用法
函数原型为:
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
参数 sockfd 表示一个套接口的描述字;
参数 level 选项定义的层次,支持 SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP 和 IPPROTO_IPV6;
参数 optname 需设置的选项;
参数 optval 指针,指向存放选项值的缓冲区;
参数 optlen 存放 optval 缓冲区的长度。
注意:以下操作全部必须放在 bind 之前执行,另外通常用于 UDP 的。
(1)如果在已经处于 ESTABLISHED 状态下的 Socket (一般由端口号和标志符区分)调用 closesocket(一般不会立即关闭而要经历 TIME_WAIT 的过程)后想继续重用该 Socket ,使用如下代码:
int reuse = 1;
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse, sizeof(int));
(2)如果要已经处于连接状态的 Socket 在调用 closesocket 后强制关闭,不经历 TIME_WAIT 的过程,使用如下代码:
int reuse = 0;
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse, sizeof(int));
(3)在 send() 和 recv() 过程中有时由于网络状况等原因,发收不能按照预期进行,需设置收发时限,使用如下代码:
int nNetTimeout = 1000; //1s
setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, (char *)&nNetTimeout, sizeof(int)); //发送时限
setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, (char *)&nNetTimeout, sizeof(int)); //接收时限
(4)在 send() 的时候,返回的是实际发送出去的字节(同步)或发送到 Socket 缓冲区的字节(异步),系统默认的状态发送和接收一次为 8688 字节(约为 8.5Bit);在实际的过程中发送数据和接收数据量比较大,可以设置 Socket 缓冲区,从而避免了 send() 和 recv() 的不断循环收发:
//接收缓冲区
int nRecvBuf = 32*1024; //32K
setsockopt(s, SOL_SOCKET, SO_RCVBUF, (const char *)&nRecvBuf, sizeof(int));
//发送缓冲区
int nSendBuf = 32*1024; //32K
setsockopt(s, SOL_SOCKET, SO_SNDBUF, (const char *)&nSendBuf, sizeof(int));
(5)如果在发送数据时,希望不经历由系统缓冲区到 Socket 缓冲区的复制而影响程序的性能:
int nZero = 0;
setsockopt(socket, SOL_SOCKET, SO_SNDBUF, (char *)&nZero, sizeof(int));
(6)同上在 recv() 完成上述功能(默认情况是将 Socket 缓冲区的内容复制到系统缓冲区):
int sZero = 0;
setsockopt(socket, SOL_SOCKET, SO_RCVBUF, (char *)&sZero, sizeof(int));
(7)一般在发送 UDP 数据报的时候,希望该 Socket 发送的数据具有广播特性:
int bBroadcast = 1;
setsockopt(s, SOL_SOCKET, SO_BROADCAST, (const char *)&bBoroadcast, sizeof(int));