嵌入式Linux系统编程学习之三十四 Socket 编程


一、使用 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));

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值