Socket编程基础

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


rtsp实时音视频开发实战课程:
socekt实现TCP/UDP通信代码实现

前言

程序进程间通信方式之一就有socket,进程/程序可以通过socket和其他进程/程序进行数据的发送和接受。socket是网络编程的一种机制,socket使用TCP/UDP协议进行通信的。socket是程序中网络编程的一个组件,是对TCP/IP的封装。利用socket可以编写基于TCP/UDP的的网络通信代码。本篇文章主要介绍socket主要的网络编程API接口。


一、基于TCP的socket通信流程

socket通信是客服端-服务器模型,服务器监听,客户端请求,双方连接确认。
服务器端主要流程如下:
  1、创建socket,调用socket()函数;
  2、绑定主机IP地址、端口等信息到socket上,调用函数bind();
  3、监听客户端的连接,调用函数listen();
  4、接收客户端的连接,调用函数accept();
  5、收发数据,使用用函数send()和recv();
  6、关闭网络连接;
  7、关闭监听;
客户端的主要流程如下:
1、创建socket,调用socket()函数;
  2、设置要服务器IP地址和端口等属性;
  4、连接服务器,调用函数connect();
  5、收发数据,用函数send()和recv();
  6、关闭网络连接;
在这里插入图片描述

二、基于UDP的socket通信流程

服务器端主要流程是:
  1、创建socket,调用函数socket();
  2、绑定主机IP地址、端口等信息到socket上,调用函数bind();
  3、循环接收数据,调用函数recvfrom();
  4、关闭网络连接。
客户端主要流程是:
  1、创建一个socket,调用函数socket();
  2、设置对方的IP地址和端口等属性;
  3、发送数据,用函数sendto();
  4、关闭网络连接;
在这里插入图片描述

三、TCP协议下socket编程主要API接口介绍

有关socket API的介绍可以在https://man7.org/中搜索到:
  如socket: https://man7.org/linux/man-pages/man2/socket.2.html
  此外也可以在linux源码中查看到socket的实现,linux内核源码中相关api接口函数的实现函数名前包含"_sys",
   如socket()函数在linux内核中实现是__sys_socket()
   https://github.com/torvalds/linux/blob/master/net/socket.c
   https://github.com/torvalds/linux/blob/master/include/linux/socket.h
   通常socket的使用需要包含#include <sys/socket.h>头文件

1、int socket(int domain, int type, int protocol);

socket函数的说明:https://man7.org/linux/man-pages/man2/socket.2.html
该函数用于创建一个通信端点;
domain参数指定一个通信域,主要有
  AF_UNIX:常用于本地通信
  AF_INET: 使用IPv4 互联网协议
  AF_INET6: 使用IPv6 Internet 协;
  其他。
type 类型,它指定通信语义:
   SOCK_STREAM:提供顺序的、可靠的、双向的、基于连接的字节流。一般使用TCP方式的是时候使 用这个参数。
  SOCK_DGRAM:无连接、不可靠的消息,一般UDP方式的时候用到这个参数。
  其他。
protocol 协议类型:
  IPPROTO_TCP: 指定TCP协议
  IPPROTO_UDP:指定UDP协议
  其他。
  protocol 在为0的情况下可以根据type 类型决定socket的通信类型,比如type 为SOCK_STREAM协议类型是TCP,type 为SOCK_DGRAM协议类型是UDP。
返回值:
  成功完成后, socket() 返回一个非负整数,即套接字文件描述符。否则返回值 -1 并设置 errno以指示错误 。

2、int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

bind函数的说明:https://man7.org/linux/man-pages/man2/bind.2.html
该函数一般用于服务端,将IP地址绑定到套接字。
**sockfd:**为socket返回的socket文件描述符。
**addr :**为要绑定的网络信息。addr 的类型是struct sockaddr:其定义如下

	 struct sockaddr {
	        sa_family_t sa_family;
	        char        sa_data[14];
	 }

在实际使用的时候根据需要(比如:需要绑定端口号和IP地址)可以使用struct sockaddr_in这个结构来替代 struct sockaddr,struct sockaddr_in结构定义如下:

	 struct sockaddr_in
	 {
	     sa_family_t sin_family; //地址协议类型,如AF_INET IPV4
	  	 uint16_t sin_port; //端口号,如554
		 struct in_addr sin_addr;//IP地址,如127.0.0.1
		 char sin_zero[8];
	 }
	 struct in_addr
	 {
	  	in_addr_t s_addr; 
	 }

addrlen:为addr结构体的长度。
返回值:
  成功时,返回零。出错时返回 -1,并设置 errno以指示错误。

3、int listen(int sockfd, int backlog);

listen函数的说明:https://man7.org/linux/man-pages/man2/listen.2.html
该函数一般用于服务端,将socket()创建的socket设置被动套接字,用于监听连接进来的socket。
  sockfd:为socket()创建的socket文件描述符。
  backlog:能够连接进来的最大socket数。该参数是一个队列最大数目用于存放连接进来的socket。
  返回值:成功时,返回零。出错时返回 -1,并设置 errno以指示错误。

4、socket、bind、listen函数使用的示例

struct  sockaddr_in inaddr;
    if (port <= 0)
	port = 554;
		
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd < 0) {
		PRINT_ERROR("create socket failed ret %d,%d: %s\n",ret, errno,strerror(errno));
	    return -1;
	}

    int  ret = 0;
	memset(&inaddr, 0, sizeof(inaddr));
	inaddr.sin_family = AF_INET;
	inaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	inaddr.sin_port = htons(port);
	ret = bind(sockfd, (struct sockaddr*)&inaddr, sizeof(inaddr));
	if (ret < 0) {
		PRINT_ERROR("bind socket to address failed : %s\n", strerror(errno));
		return -1;
	}
	
	ret = listen(sockfd, 20); //设置最大监听为20个
	if (ret < 0) {
		PRINT_ERROR("listen socket failed : %s\n", strerror(errno));
		return -1;
	}

	return sockfd;

关于网络字节序的说明:
  一般在将端口号和IP地址绑定到socket的时候需要使用htonl/htons将本地字节序转为网络字节序,其中htonl是将long形数据转网络字节序;htons是将short类型数据转为网络字节序。将本地字节序转为网络字节序是因为网络通信两端的机器可能存在不同的字节序类型,即大端或者小端;大端机和小端机在数据存储上顺序不同,如大端机存储顺序是地址的低位存储值的高位,小端机存储顺序是地址的低位存储值的低位。网络字节顺序采用大端机存储顺序方式;网络数据在双方传输接收后可以明确知道数据的存储顺序,可以保证数据在不同主机之间传输时能够被正确解析。

5、int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict addrlen);

accept函数的说明:https://man7.org/linux/man-pages/man2/accept.2.html
该函数一般用于服务端,用于接受一个套接字的连接,即完成客户端和服务端的连接过程(包含TCP的3次握手)。该函数从listen监听队列里面获取第一个连接,创建一个新的套socket,并返回socket文件描述符。
  sockfd:为socket()创建的被动socket文件描述符。
  addr:其类型是struct sockaddr结构体,结构体主要成员包含对端的IP地址,对端的端口号,addr包含了客户端的IP地址和端口号。
  addrlen:用于返回addr地址长度。
  返回值:成功时,返回socket文件描述符。出错时返回 -1,并设置 errno以指示错误

6、int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

connect函数说明:https://man7.org/linux/man-pages/man2/connect.2.html
该函数一般用于客户端,用于连接一个socket,向服务端发起连接。
  sockfd:为socket创建的socket文件描述符。
  addr:为要连接的对方的IP/端口号信息等,其结构体为struct sockaddr ,在实际使用的时候可以使用struct sockaddr_in这个结构来替代 struct sockaddr。
  addrlen:为addr结构体的长度。
  返回值:成功时,返回0。出错时返回 -1,并设置 errno以指示错误
connect函数的使用代码示例:

    struct sockaddr_in inaddr;
	short port=554;`
	memset(&inaddr, 0, sizeof(inaddr));
	inaddr.sin_family = AF_INET;
	inaddr.sin_addr.s_addr = inet_addr(peer_ip);//对方IP地址
	inaddr.sin_port = htons(port);//对方端口号
	ret = connect(*sockfd, (struct sockaddr*)&inaddr, sizeof(inaddr));
	if (ret < 0) {
		return -1;
	}    

7、ssize_t recv(int sockfd, void *buf, size_t len, int flags);

recv函数说明:https://man7.org/linux/man-pages/man2/recv.2.html
该函数通常用于TCP方式下接收对方的数据。
  sockfd:在服务端为accept()返回的文件描述符,在客户端为socket()函数返回的文件描述符。
  buf:要接收数据的缓存
  len:要接收数据的长度(不大于buf的大小)
  flags:为函数执行的附件标识,如MSG_DONTWAIT ,启用非阻塞操作,表示没有数据可接收不等待立刻返回。
  返回值:成功返回读入的字节数。如果连接已中止,返回0。如果发生错误,返回-1。
 与recv函数类似的函数还有recvfrom函数,如下:
 ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags,
struct sockaddr *restrict src_addr,
socklen_t *restrict addrlen);
通常UDP方式下的数据接受用recvfrom函数。函数的参数sockfd、buf、len、flags和recv一样。
   src_addr:为已经连接的对方的IP/端口号信息等,其结构体为struct sockaddr ,在实际使用的时候可以使用struct sockaddr_in这个结构来替代 struct sockaddr。
  addrlen:为src_addr结构体的长度。
  返回值:成功返回读入的字节数。如果发生错误,返回-1。

8、ssize_t send(int sockfd, const void *buf, size_t len, int flags);

send函数说明:https://man7.org/linux/man-pages/man2/send.2.html
该函数用于往对方socket发送数据。
  sockfd:在服务端为accept()返回的文件描述符,在客户端为socket()函数返回的文件描述符。
  buf:要发送数据的缓存
  len:要发送数据的长度(不大于buf的大小)
  flags:为函数执行的附件标识,如MSG_DONTWAIT ,启用非阻塞操作。
  返回值:成功返回发送的字节数。出错时返回 -1,并设置 errno以指示错误。
与send函数类似的函数有sendto
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
通常UDP方式下的数据发送用sendto函数。函数的参数sockfd、buf、len、flags和send一样。
  dest_addr:为已经连接的对方的IP/端口号信息等,其结构体为struct sockaddr ,在实际使用的时候可以使用struct sockaddr_in这个结构来替代 struct sockaddr。
  addrlen:为dest_addr结构体的长度。
  返回值:成功返回发送的字节数。出错时返回 -1,并设置 errno以指示错误。
关于 flags的使用说明,如在MSG_DONTWAIT 的使用需要在每次调用recv/send函数的时候手动写入MSG_DONTWAIT 参数。如果有多个函数或者多个线程使用recv/send函数,就要写多次MSG_DONTWAIT 给flgs参数。代码比较多的时候可能容易遗忘。除此方法启用非阻塞操作外,在linux下可以使用fcntl函数API来设置socket的非阻塞操作。代码示例如下:

flgs= fcntl(sockfd, F_GETFL, 0);
if (flgs< 0) {
	PRINT_WARN("fcntl F_GETFL failed\n");
} else {
	flgs|= O_NONBLOCK;
	flgs= fcntl(sockfd, F_SETFL, flgs);
	if (flgs< 0) {
		PRINT_WARN("fcntl F_SETFL failed\n");
	}
}

fcntl(sockfd, F_GETFL, 0)通过使用F_GETFL来获取socket的“flgs”。flgs|= O_NONBLOCK;通过与操作将flas加上非阻塞的操作。通过fcntl(sockfd, F_SETFL, flgs)设置socket非阻塞的操作。
在windows下可以使用ioctlsocket接口来设控制socekt非阻塞的操作。代码示例如下:

unsigned long nonblocked = 1;
ret = ioctlsocket(sockfd, FIONBIO, &nonblocked);
if (ret < 0) {
	PRINT_WARN("ioctlsocket FIONBIO failed\n");
}

在通过ioctlsocket或fcntl设置了socekt的非阻塞的操作后调用recv/send函数时候flgs参数值为0即可。

四、socket编程其他有用的API

1、WSAStartup(WORD sockVersion,LPWSADATA lpWSAData)

在windows平台上创建socket前必须调用WSAStartup函数。WSAStartup函数完成对Winsock服务的初始化,之后才能调用socket的Api接口。
  sockVersion:Windows Sockets API提供的调用方可使用的最高版本号。高位字节代表次版本号,低位字节代表主版本号,可以使用MAKEWORD()来构造版本号,如MAKEWORD(2,2)。
  lpWSAData:WSAStartup函数调用后返回的 Windows Sockets数据。
  当使用了WSAStartup函数后socket关闭后须调用WSACleanup()将其从Windows Sockets的实现中注销,并释放分配的资源。

2、int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);

setsockopt函数说明:https://man7.org/linux/man-pages/man2/setsockopt.2.html
该函数设置socket的属性。一般先调用getsockop(int sockfd, int level, int optname, void *restrict optval, socklen_t *restrict optlen)函数先获取socket的属性,如果属性已经设置无需再设置。
   sockfd:socket的文件描述符。
   level:指定的协议层。支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6。
   optname:通常为要设置的属性名称,主要有;
        SO_SNDBUF:设置发送缓存区大小
        SO_KEEPALIVE:设置保活开启/关闭。当SO_KEEPALIVE开启时,可以检测对方主机是否异常(如崩溃),避免一直阻塞在连接之中。
        TCP_KEEPINTVL:设置SO_KEEPALIVE之后一般还有设置TCP_KEEPINTVL来设置异常时探测数据发送的时间间隔。
        TCP_KEEPIDLE:设置SO_KEEPALIVE之后一般设置TCP_KEEPIDLE来设置保活时间阈值,如设置60,则超过60秒没有接收到对方数据认为对方异常,开始保活探测。
        TCP_KEEPCNT:设置尝试探测对方保活的最大次数,超出这个此时关闭连接。
        SO_LINGER:关闭socket的时候是否等待数据发送完。
        TCP_NODELAY:tcp关闭Nagle算法,允许小包的发送,即数据不管多大,有数据TCP就发送,可以达到发送小数据包低延时的目的。
        SO_REUSEADDR:地址重复使用。当程序推出后,比如kill掉之后又从新启动程序可能会出现“Address already in use”的错误提示,在创建scoket设置SO_REUSEADDR则可以解决该错误。
       
   optval:设置值的地址。
   optlen:optval缓冲区长度。
setsockopt函数的使用示例:

int flag = 1; /* Disable the Nagle (TCP No Delay) algorithm */
ret = setsockopt( sockfd, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(flag) );
if (ret == -1) {
  PRINT_WARN("Couldn't setsockopt(TCP_NODELAY)\n");
}
int sendbufsiz = 1024 * 512; //设置发送缓冲区512k
ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, (const char*)&sndbufsiz, sizeof(sndbufsiz));
if (ret < 0) {
	PRINT_WARN("setsockopt SO_SNDBUF failed\n");
}
int keepalive  = 1;          //开启保活
ret = setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, (const char*)&keepalive, sizeof(keepalive));
if (ret < 0) {
	PRINT_WARN("setsockopt SO_KEEPALIVE failed\n");
}
int keepidle   = 60;         //设置保活时间60秒
ret = setsockopt(sockfd, SOL_TCP, TCP_KEEPIDLE, (const char*)&keepidle, sizeof(keepidle));
if (ret < 0) {
	PRINT_WARN("setsockopt TCP_KEEPIDLE failed\n");
}
int keepinterval = 5;        //设置发送保活数据的时间间隔5秒
ret = setsockopt(sockfd, SOL_TCP, TCP_KEEPINTVL, (const char*)&keepinterval, sizeof(keepinterval));
if (ret < 0) {
	PRINT_WARN("setsockopt TCP_KEEPINTVL failed\n");
}
int keepcount    = 3;        //设置发送保活数据的最大次数3,超过这个次数会关闭连接。
ret = setsockopt(sockfd, SOL_TCP, TCP_KEEPCNT, (const char*)&keepcount, sizeof(keepcount));
if (ret < 0) {
	PRINT_WARN("setsockopt TCP_KEEPCNT failed\n");
}
struct linger ling;
memset(&ling, 0, sizeof(ling));
ling.l_onoff  = 1;
ling.l_linger = 0;
ret = setsockopt(sockfd, SOL_SOCKET, SO_LINGER, (const char*)&ling, sizeof(ling)); 
if (ret < 0) {
	PRINT_WARN("setsockopt SO_LINGER failed\n");
}

在windows下设置保活的接口为WSAIoctl(),使用示例如下:

struct tcp_keepalive keep_alive
keep_alive.onoff = 1;
keep_alive.keepalivetime     = 60000;      //保活时间60秒
keep_alive.keepaliveinterval = 3000;   //发送保活数据的时间间隔3秒
ret = WSAIoctl(sockfd, SIO_KEEPALIVE_VALS, &keep_alive, sizeof(keep_alive), 
	&alive_out, sizeof(alive_out), &alive_retlen, NULL, NULL);
if (ret == SOCKET_ERROR) {
	PRINT_WARN("WSAIoctl SIO_KEEPALIVE_VALS is failed\n");
}
  • 0
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值