socket简单使用(Android、c、QT不同场景下使用)

一:Socket使用场景:

  1. socket做网络通信使用,例如游戏中的聊天,IM聊天(QQ微信等社交),这些是大型的场景;
  2. 还有一些是次一等的场景,例如一套本地使用的软件,需要连接手机与pc程序,AS与Android手机apk调试信息应该就是使用这个方式;
  3. 再小一个等级就是一个软件项目中,跨进程的通信,因为有些项目的设计导致数据在不同的进程中频繁传递,此时以socket通信也是一个办法,当然还有很多binder通信,管道,共享内存,AIDL。

二:使用示例

我在这里遇到的是第二种使用场景,多个设备的应用程序需要互通数据进行通信,因此选取socket,原因是win系统、Linux系统、都支持socket通信,诸多编程语言也都支持socket(例如Android应用的java语言、QT的c++语言)。这些语言都集成了socket,使用起来很方便,但是在不同系统或者说开发工具中需要多次开发功能完全相同的代码,因此我们可以只使用标准c的api开发socket功能,造这么一个轮子。java语言可以使用jni去调用so库,而其余环境中也能使用so库去直接调用。

这里有一个问题,我在Android中写demo的时候发现,client端给server端发送数据的时候,server端的数据log无法打印出来,追踪log发现,数据其实发送过去了,server端也收到了,只是server端认为你还在写数据,InputStream inputStream = socket.getInputStream();read input数据的while循环没有break,因此数据的收发在java编程的环境中需要指定收发的结束符,一旦server端收到了这个两端约定的结束符,就要结束这个while,从而让程序继续执行。

这里介绍几个java语言的socket api:

1、ServerSocket(int port) //见名知意直接用,这就是server端
2、Socket accept() //由第一步实例出来的对象调用此方法,这个函数意思是与serverSocket连接成功的对端clientSocket,因此返回的也是对端的clientSocket,此函数是阻塞的函数,必须在子线程调用,否则apk会crash,log会直接提示不可以在UI线程调用此函数(具体是什么来着忘记了,反正坚决不能在UI线程调用)
3、socket.getInputStream() //收数据的函数,这里有个问题就是上述说明的,其他没什么特别,与java操作IO流读文件一样
4、socket.getOutputStream() //写数据的函数,与java操作IO流写文件一样,write完成后调用flush刷新数据。
5、只管使用也就是这几个函数,细致的开发需求都是依照业务详细补充的,无须赘述,如果想要了解java中socket实现原理的话,那就必须往下看,标准c的socket是怎么实现的。

三:标准c函数的socket

这是一张网上的经典例图

3.1 先来概括流程再分别介绍函数,我们力求在整体上有个大致的印象,socket两端是怎么回事,再详细介绍其函数api。

    1、这是一张网上的经典例图,基本上能看懂,本质上就是c/s两端都有socket,c端的socket使用connect连接到s端server上,从而使两端能收发数据,当然前提是server端必须已经启动
    2、那么server是怎么启动的,也是使用一个socket,然后给这个socket,bind绑定IP地址和端口号,再使用listen监听这个端口号,然后使用accept函数,等待c端的某个socket来连接,其实这个步骤应该是先2后1,不过为了跟图片从左到右匹配,先说了client端,才将的server端,
    3、当c与s端连接到一起,这时就可以相互收发数据,谁收谁发无所谓,都可以,最后不用的时候要记得close,这就是底层c方式实现socket的大体流程

3.2 下面来介绍函数api,

1、不同操作系统中,socket实例化稍微有点区别,其实本质一样,只是Linux的理念是一切皆文件,所以实例化出来的对象有点差异:
    1.1 Linux环境下:
        #include <sys/socket.h>
        #include <arpa/inet.h>
        int mSocketFD = socket(AF_INET, SOCK_STREAM, 0)//这里的fd就很能说明问题,它是一个文件描述符,恰好对上了Linux的一切皆文件理念
    1.2 win环境下:
        #include <winsock2.h>
        #include <WS2tcpip.h>
        SOCKET sock_client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//它的返回值是真实的对象,
    可以清晰看到,差异就在返回值上,构造函数的参数是没有任何区别的,当然include头文件是不同的,一个是win环境,一个是Linux,这其实是句废话,对于socket实例对象来说,也就是 fd这个int类型返回值还是socket对象,对于后续的socket标准c函数调用没有任何区别,我们在学习的时候可以当它完全一样,只是语法写法的区别。那么重点是构造参数:这些构造参数都是头文件带进来的系统宏,无须我们手动声明初始化
—————————————————————————————

    int socket(int af,int type,int protocol)
        第一个参数af:规定IP地址族,也就是说,你是使用IPv4还是v6,详细介绍如下:

地址族含义
AF_INETIPv4网络协议中采用的地址族
AF_INET6IPv6网络协议中采用的地址族
AF_LOCAL本地通信中采用的UNIX协议的地址族(用的少)

        第二个参数:套接字类型。数据格式是什么,不同的网络传输协议,它的数据格式不同。这里其实就让你选择网络协议是tcp/ip还是udp。

地址族含义
SOCKET_RAW原始套接字(SOCKET_RAW)允许对较低层次的协议直接访问,比如IP、 ICMP协议
SOCK_STREAMSOCK_STREAM是数据流,一般为TCP/IP协议的编程
SOCK_DGRAMSOCK_DGRAM是数据报,一般为UDP协议的网络编程

        第三个参数:最终采用的协议。常见的协议有IPPROTO_TCP、IPPTOTO_UDP。如果第二个参数选择了SOCK_STREAM,那么采用的协议就只能是IPPROTO_TCP;如果第二个参数选择的是SOCK_DGRAM,则采用的协议就只能是IPPTOTO_UDP。
返回值:如果是-1,就说明打开这个节点失败了。大于0的值是成功,应该就代表打开的这个socket文件描述符

3.2.2 第二个函数 bind(int fd,const struct sockaddr *name,int namelen)

第一个参数fd,socket文件描述符,fd即套接字创建时返回的对象,win环境下:bind(SOCKET s,const struct sockaddr *name,int namelen),其实Linux环境的fd和win环境的socket对象是一个意思。

第二个参数:struct sockaddr,显然这是一个关于IP地址的结构体,
struct sockaddr {
u_short sa_family;/* Common data: address family and length. /
char sa_data[14];/
Address data. */
};
一般好像都是用这个结构体的变种,sockaddr_in这个结构体,
struct sockaddr_in {
short sin_family;//前面介绍的地址族
u_short sin_port;//16位的TCP/UDP端口号
struct in_addr sin_addr;//32位的IP地址
char sin_zero[8];//不使用
};
说明:
1、sin_family地址族,这没什么说的,常量
2、sin_port端口号,使用htons(int port)可以将int转为这里的u_short 结构体
3、in_addr 32位的IP地址,我们来看看这个结构体
typedef struct in_addr {
union {
struct { u_char s_b1, s_b2, s_b3, s_b4; } S_un_b;
struct { u_short s_w1, s_w2; } S_un_w;
u_long S_addr;
} S_un;
} IN_ADDR, *PIN_ADDR, *LPIN_ADDR;
使用方式是这样的:
    listen_addr.sin_addr.S_un.S_addr = INADDR_ANY;//INADDR_ANY转换过来就是0.0.0.0,泛指本机的意思,反正就是你网卡IP地址是多少,就能给socket绑定成多少的addr,为什么不直接指定,是因为计算机可能有三五块网卡,那么每一个的IP地址是不同的,这个inaddr_any会把所有的IP地址全给绑定到socket上去,这样只需关注port端口号就行。第三个参数,就是长度sizeof(addr),无须赘述,返回值如果是-1,代表失败!

关于socket的IP地址还有一个概念:网络传输时,统一都是以大端序的网络字节序方式传输数据,要注意的是s_addr是一种uint32_t类型的数据我们习惯的192.168.2.101这类型是十进制的IP地址,但是这里的socket addr是32位,我们需要转换的标准c提供了一些函数供我们转换:
in_addr_t inet_addr (const char *__cp) __THROW;
使用示例: addr_server.sin_addr.s_addr = inet_addr(ipAddress);
//或者
int inet_aton (const char *__cp, struct in_addr *__inp) __THROW; //windows无此函数
还有一些转换函数,我直接抄上来:
/*Functions to convert between host and network byte order.
Please note that these functions normally take unsigned long int' orunsigned short int’ values as arguments and also return them. But
this was a short-sighted decision since on different systems the types
may have different representations but the values are always the same. */
// h代表主机字节序
// n代表网络字节序
// s代表short(4字节)
// l代表long(8字节)
extern uint32_t ntohl (uint32_t __netlong) __THROW attribute ((const));
extern uint16_t ntohs (uint16_t __netshort)
__THROW attribute ((const));
extern uint32_t htonl (uint32_t __hostlong)
__THROW attribute ((const));
extern uint16_t htons (uint16_t __hostshort)

3.2.3 第三个函数 int listen(int __fd, int __n)

第一个参数:socket文件描述符__fd,分配所需的信息后的套接字。
第二个参数:连接请求的队列长度,如果为6,表示队列中最多同时有6个连接请求
这个函数的fd(socket套接字对象)就相当于一个门卫,对连接请求做处理,决定是否把连接请求放入到server端维护的一个队列中去。同样的,返回值-1肯定是失败

3.2.4 第四个函数 int accept (int __fd, struct sockaddr *addr, socklen_t *addr_len);

listen()中的sock(__fd : socket对象)发挥了服务器端接受请求的门卫作用,此时为了按序受理请求,给客户端做相应的回馈,连接到发起请求的客户端,此时就需要再次创建另一个套接字,该套接字可以用accept 函数创建,
函数成功执行时返回socket文件描述符,失败时返回-1。
第一个参数:socket文件描述符__fd。
第二个参数:保存发起连接的客户端的地址信息。
第三个参数: 保存该结构体的长度。
使用示例:
sockaddr_in caddr;
socklen_t size = sizeof(sockaddr_in);
int cfd;
cfd = accept(fd, (sockaddr*)&caddr, &size);

3.2.5 第五个函数 int connect (int socket, struct sockaddr* servaddr, socklen_t addrlen);

几个参数的意义和前面的accept函数意义一样。要注意的是服务器端收到连接请求的时候并不是马上调用accept()函数,而是把它放入到请求信息的等待队列中。返回值为-1说明失败了
使用示例:
sockaddr_in addr_server;
addr_server.sin_family = AF_INET;
addr_server.sin_port = htons(port);
addr_server.sin_addr.s_addr = inet_addr(“192.168.10.185”);
int code = connect(mCurSocket, (sockaddr *)&addr_server, sizeof(sockaddr_in));

3.2.6 第六个函数 int send (int sockfd, const void *buf, size_t nbytes, int flag) ;
3.2.7 第七个函数 int recv(int sockfd, void *buf, size_t nbytes, int flag) ;

基本上从字面意思就能看出来,最后一个参数flag,是传输数据时可指定的信息,一般设置为0。
使用示例:
发送:
int ret = 0;
ret = send(mCurrentClientFD, sendMess, sizeof(sendMess), 0);

接收:数据就在resp
char resp[DATA_LENGTH];
int ret;
ret = recv(fd, (char *) resp, DATA_LENGTH, 0);
说明,发送当然可以多次发送,而接受数据,如果数据非常大,char resp[DATA_LENGTH]装不下那么大数据的时候,recv这个函数就会调用多次,

3.2.8 第八个函数int close (int __fd);

退出连接,此时要注意的是:调用close()函数即表示向对方发送了EOF结束标志信息。也就是说任意哪一端的socket调用close,对端会recv到返回值不是0就是-1。socket如何判断连接状态,我认为有它就够用了,只要recv到小于等于0,就判定socket连接断开。
就是说recv和send的返回值是很有用的,不仅可以判断收发成功或者失败,也可以区分socket是否断开连接

3.3 socket设置某些属性

int getsockopt (int sock, int __level, int __optname, void *__optval, socklen_t *optlen) ;
int setsockopt (int sock, int __level, int __optname,const void *__optval, socklen_t __optlen)
第一个参数sock,就是socket的文件描述符 fd,
第二个参数 level:可选项的协议层

协议层功能
SOL_SOCKET套接字相关通用可选项的设置
IPPROTO_IP在IP层设置套接字的相关属性
IPPROTO_TCP在TCP层设置套接字相关属性

第三个参数 __optname ** :要查看的可选项名,几个主要的选项如下

选项名说明数据类型所属协议层
SO_RCVBUF接收缓冲区大小intSOL_SOCKET
SO_SNDBUF发送缓冲区大小intSOL_SOCKET
SO_RCVLOWAT接收缓冲区下限intSOL_SOCKET
SO_SNDLOWAT发送缓冲区下限intSOL_SOCKET
SO_TYPE获得套接字类型(这个只能获取,不能设置)intSOL_SOCKET
SO_REUSEADDR是否启用地址再分配,主要原理是操作关闭套接字的Time-wait时间等待的开启和关闭intSOL_SOCKET
IP_HDRINCL在数据包中包含IP首部intIPPROTO_IP
IP_MULTICAST_TTL生存时间(Time To Live),组播传送距离intIPPROTO_IP
IP_ADD_MEMBERSHIP加入组播intIPPROTO_IP
IP_OPTINOSIP首部选项intIPPROTO_IP
TCP_NODELAY不使用Nagle算法intIPPROTO_TCP
TCP_KEEPALIVETCP保活机制开启下,设置保活包空闲发送时间间隔intIPPROTO_TCP
TCP_KEEPINTVLTCP保活机制开启下,设置保活包无响应情况下重发时间间隔intIPPROTO_TCP
TCP_KEEPCNTTCP保活机制开启下,设置保活包无响应情况下重复发送次数intIPPROTO_TCP
TCP_MAXSEGTCP最大数据段的大小intIPPROTO_TCP

第三个参数 optval:保存查看(get)/更改(set)的结果
第四个参数optlen : 传递第四个参数的字节大小

3.4 插入一个,标准c方式创建一个工作线程

pthread_t accept_t;
pthread_create(pthread_t *th,NULL,funcName,funcParam);
第一个参数:就是声明的pthread_t
第二个参数:我一直是传NULL
第三个参数:传进来一个函数,这个函数会运行在创建的工作线程中
第四个参数:给参数三传递什么参数,放到这里去传递

四:总结

大体上的函数就是这些,测试demo的话也是将上述的一些函数套起来用基本上就行,只是当使用socket开发真正的软件,需要考虑:

  1. server端是否可以和众多个client端同时通信,如果有这样的需求,那么首要使用死循环去执行accept函数,当然了不能真就用死循环去写代码,否则要close整个server端该怎么办,这个死循环它根本停不下来,就是意思说,c/s两端要多对一,accept函数必须一直在执行
  2. c/s两端要可以不停的接收数据,recv函数也必须一直在执行,就像handler的loop,因此也需要类似死循环的方式去编写这块代码,

我写了一个简单的c/s两端demo:GitHub传送门

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值