Socket开发实战

Socket实际开发中的应用


Socke的概念:

socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。

☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆

socket 编程是基于 TCP 和 UDP 协议的,它们的层级关系如下图所示:

【扩展阅读】开放式系统(Open System)

把协议分成多个层次有哪些优点?协议设计更容易?当然这也足以成为优点之一。但是还有更重要的原因,就是为了通过标准化操作设计成开放式系统。

标准本身就是对外公开的,会引导更多的人遵守规范。以多个标准为依据设计的系统称为开放式系统(Open System),我们现在学习的 TCP/IP 协议族也属于其中之一。

开放式系统具有哪些优点

路由器用来完成 IP 层的交互任务。某个网络原来使用 A 公司的路由器,现要将其替换成 B 公司的,是否可行?这并非难事,并不一定要换成同一公司的同一型号路由器,因为所有生产商都会按照 IP 层标准制造。

再举个例子。大家的计算机是否装有网络接口卡,也就是所谓的网卡?尚未安装也无妨,其实很容易买到,因为所有网卡制造商都会遵守链路层的协议标准。这就是开放式系统的优点。

标准的存在意味着高速的技术发展,这也是开放式系统设计最大的原因所在。实际上,软件工程中的“面向对象(Object Oriented)”的诞生背景中也有标准化的影子。也就是说,标准对于技术发展起着举足轻重的作用。


Socket:套接字

  • Socket就是为网络服务提供的一种机制
  • 通讯的两端都是Socket
  • 网络通讯其实就是Socket间的通信
  • 数据在两端socket间通过IO传输
  • HTTP协议的传输实质就是Socket通信

网络通讯要素

  1. 网络中设备的表示
  2. 不易记忆,可以用主机名
  3. 本地会换地址127.0.0.1 主机名:localhost
  4. 端口号—定位程序
  5. 用于标识进程的逻辑地址,不同进程的标识
  6. 有效端口:065535,其中01024由系统支配

TCP

TCP:全称是网络控制协议(它使两台主机能够建立连接并交换数据流)TCP能保证数据的交付,维持数据包的发送顺序。

TCP(Transmission Control Protocol: 传输控制协议)是一种面向连接的、可靠的、基于字节流的通信协议,数据在传输前要建立连接,传输完毕后还要断开连接。

客户端在收发数据前要使用 connect() 函数和服务器建立连接。建立连接的目的是保证IP地址、端口、物理链路等正确无误,为数据的传输开辟通道。

TCP建立连接时要传输三个数据包,俗称三次握手(Three-way Handshaking)。可以形象的比喻为下面的对话:

  • [Shake 1] 套接字A:“你好,套接字B,我这里有数据要传送给你,建立连接吧。”
  • [Shake 2] 套接字B:“好的,我这边已准备就绪。”
  • [Shake 3] 套接字A:“谢谢你受理我的请求。”

TCP数据报结构

带阴影的几个字段需要重点说明一下:

  1. 序号:Seq(Sequence Number)序号占32位,用来标识从计算机A发送到计算机B的数据包的序号,计算机发送数据时对此进行标记。

  2. 确认号:Ack(Acknowledge Number)确认号占32位,客户端和服务器端都可以发送,Ack = Seq + 1。

  3. 标志位:每个标志位占用1Bit,共有6个,分别为 URG、ACK、PSH、RST、SYN、FIN,具体含义如下:

  • URG:紧急指针(urgent pointer)有效。
  • ACK:确认序号有效。
  • PSH:接收方应该尽快将这个报文交给应用层。
  • RST:重置连接。
  • SYN:建立一个新连接。
  • FIN:断开一个连接。

注解:对英文字母缩写的总结:Seq 是 Sequence 的缩写,表示序列;Ack(ACK) 是 Acknowledge 的缩写,表示确认;SYN 是 Synchronous 的缩写,愿意是“同步的”,这里表示建立同步连接;FIN 是 Finish 的缩写,表示完成。

连接的建立(三次握手)

客户端调用 socket() 函数创建套接字后,因为没有建立连接,所以套接字处于CLOSED状态;服务器端调用 listen() 函数后,套接字进入LISTEN状态,开始监听客户端请求。

这个时候,客户端开始发起请求

  1. 当客户端调用 connect() 函数后,TCP协议会组建一个数据包,并设置 SYN 标志位,表示该数据包是用来建立同步连接的。同时生成一个随机数字 1000,填充“序号(Seq)”字段,表示该数据包的序号。完成这些工作,开始向服务器端发送数据包,客户端就进入了SYN-SEND状态。

  2. 服务器端收到数据包,检测到已经设置了 SYN 标志位,就知道这是客户端发来的建立连接的“请求包”。服务器端也会组建一个数据包,并设置 SYN 和 ACK 标志位,SYN 表示该数据包用来建立连接,ACK 用来确认收到了刚才客户端发送的数据包。

服务器生成一个随机数 2000,填充“序号(Seq)”字段。2000 和客户端数据包没有关系。

服务器将客户端数据包序号(1000)加1,得到1001,并用这个数字填充“确认号(Ack)”字段。

服务器将数据包发出,进入SYN-RECV状态。

  1. 客户端收到数据包,检测到已经设置了 SYN 和 ACK 标志位,就知道这是服务器发来的“确认包”。客户端会检测“确认号(Ack)”字段,看它的值是否为 1000+1,如果是就说明连接建立成功。

接下来,客户端会继续组建数据包,并设置 ACK 标志位,表示客户端正确接收了服务器发来的“确认包”。同时,将刚才服务器发来的数据包序号(2000)加1,得到 2001,并用这个数字来填充“确认号(Ack)”字段。

客户端将数据包发出,进入ESTABLISED状态,表示连接已经成功建立。

  1. 服务器端收到数据包,检测到已经设置了 ACK 标志位,就知道这是客户端发来的“确认包”。服务器会检测“确认号(Ack)”字段,看它的值是否为 2000+1,如果是就说明连接建立成功,服务器进入ESTABLISED状态。

至此,客户端和服务器都进入了ESTABLISED状态,连接建立成功,接下来就可以收发数据了。

最后的说明

三次握手的关键是要确认对方收到了自己的数据包,这个目标就是通过“确认号(Ack)”字段实现的。计算机会记录下自己发送的数据包序号 Seq,待收到对方的数据包后,检测“确认号(Ack)”字段,看Ack = Seq + 1是否成立,如果成立说明对方正确收到了自己的数据包。


详细分析TCP数据的传输过程

建立连接后,两台主机就可以相互传输数据了

举例主机A分2次(分2个数据包)向主机B传递200字节的过程。首先,主机A通过1个数据包发送100个字节的数据,数据包的 Seq 号设置为 1200。主机B为了确认这一点,向主机A发送 ACK 包,并将 Ack 号设置为 1301。

为了保证数据准确到达,目标机器在收到数据包(包括SYN包、FIN包、普通数据包等)包后必须立即回传ACK包,这样发送方才能确认数据传输成功。

此时 Ack 号为 1301 而不是 1201,原因在于 Ack 号的增量为传输的数据字节数。假设每次 Ack 号不加传输的字节数,这样虽然可以确认数据包的传输,但无法明确100字节全部正确传递还是丢失了一部分,比如只传递了80字节。因此按如下的公式确认 Ack 号:

Ack号 = Seq号 + 传递的字节数 + 1

与三次握手协议相同,最后加 1 是为了告诉对方要传递的 Seq 号。


下面分析传输过程中数据包丢失的情况

TCP套接字数据传输过程中发生错误

通过 Seq 1301 数据包向主机B传递100字节的数据,但中间发生了错误,主机B未收到。经过一段时间后,主机A仍未收到对于 Seq 1301 的ACK确认,因此尝试重传数据。

为了完成数据包的重传,TCP套接字每次发送数据包时都会启动定时器,如果在一定时间内没有收到目标机器传回的 ACK 包,那么定时器超时,数据包会重传。

数据包丢失的情况,也会有 ACK 包丢失的情况,一样会重传。

重传超时时间(RTO, Retransmission Time Out

这个值太大了会导致不必要的等待,太小会导致不必要的重传,理论上最好是网络 RTT 时间,但又受制于网络距离与瞬态时延变化,所以实际上使用自适应的动态算法(例如 Jacobson 算法和 Karn 算法等)来确定超时时间。

往返时间(RTT,Round-Trip Time)表示从发送端发送数据开始,到发送端收到来自接收端的 ACK 确认包(接收端收到数据后便立即确认),总共经历的时延。

重传次数

TCP数据包重传次数根据系统设置的不同而有所区别。有些系统,一个数据包只会被重传3次,如果重传3次后还未收到该数据包的 ACK 确认,就不再尝试重传。但有些要求很高的业务系统,会不断地重传丢失的数据包,以尽最大可能保证业务数据的正常交互。

建立连接非常重要,它是数据正确传输的前提;断开连接同样重要,它让计算机释放不再使用的资源。如果连接不能正常断开,不仅会造成数据传输错误,还会导致套接字不能关闭,持续占用资源,如果并发量高,服务器压力堪忧。

建立连接需要三次握手,断开连接需要四次握手,可以形象的比喻为下面的对话:

  • [Shake 1] 套接字A:“任务处理完毕,我希望断开连接。”
  • [Shake 2] 套接字B:“哦,是吗?请稍等,我准备一下。”
  • 等待片刻后……
  • [Shake 3] 套接字B:“我准备好了,可以断开连接了。”
  • [Shake 4] 套接字A:“好的,谢谢合作。”

客户端主动断开连接的场景:

建立连接后,客户端和服务器都处于ESTABLISED状态。这时,客户端发起断开连接的请求:

  1. 客户端调用 close() 函数后,向服务器发送 FIN 数据包,进入FIN_WAIT_1状态。FIN 是 Finish 的缩写,表示完成任务需要断开连接。

  2. 服务器收到数据包后,检测到设置了 FIN 标志位,知道要断开连接,于是向客户端发送“确认包”,进入CLOSE_WAIT状态。

注意:服务器收到请求后并不是立即断开连接,而是先向客户端发送“确认包”,告诉它我知道了,我需要准备一下才能断开连接。

  1. 客户端收到“确认包”后进入FIN_WAIT_2状态,等待服务器准备完毕后再次发送数据包。

  2. 等待片刻后,服务器准备完毕,可以断开连接,于是再主动向客户端发送 FIN 包,告诉它我准备好了,断开连接吧。然后进入LAST_ACK状态。

  3. 客户端收到服务器的 FIN 包后,再向服务器发送 ACK 包,告诉它你断开连接吧。然后进入TIME_WAIT状态。

  4. 服务器收到客户端的 ACK 包后,就断开连接,关闭套接字,进入CLOSED状态。

关于 TIME_WAIT 状态的说明

客户端最后一次发送 ACK包后进入 TIME_WAIT 状态,而不是直接进入 CLOSED 状态关闭连接,这是为什么呢?

TCP 是面向连接的传输方式,必须保证数据能够正确到达目标机器,不能丢失或出错,而网络是不稳定的,随时可能会毁坏数据,所以机器A每次向机器B发送数据包后,都要求机器B”确认“,回传ACK包,告诉机器A我收到了,这样机器A才能知道数据传送成功了。如果机器B没有回传ACK包,机器A会重新发送,直到机器B回传ACK包。

UDP中的服务器端和客户端没有连接

UDP 不像 TCP,无需在连接状态下交换数据,因此基于 UDP 的服务器端和客户端也无需经过连接过程。也就是说,不必调用 listen() 和 accept() 函数。UDP 中只有创建套接字的过程和数据交换的过程。

UDP服务器端和客户端均只需1个套接字

TCP 中,套接字是一对一的关系。如要向 10 个客户端提供服务,那么除了负责监听的套接字外,还需要创建 10 套接字。但在 UDP 中,不管是服务器端还是客户端都只需要 1 个套接字。之前解释 UDP 原理的时候举了邮寄包裹的例子,负责邮寄包裹的快递公司可以比喻为 UDP 套接字,只要有 1 个快递公司,就可以通过它向任意地址邮寄包裹。同样,只需 1 个 UDP 套接字就可以向任意主机传送数据。

基于UDP的接收和发送函数

创建好 TCP 套接字后,传输数据时无需再添加地址信息,因为 TCP 套接字将保持与对方套接字的连接。换言之,TCP 套接字知道目标地址信息。但 UDP 套接字不会保持连接状态,每次传输数据都要添加目标地址信息,这相当于在邮寄包裹前填写收件人地址。

发送数据使用 sendto() 函数:

1.  ssize_t sendto(int sock, void *buf, size_t nbytes, int flags, **struct** sockaddr *to, socklen_t addrlen); //Linux

2.  int sendto(SOCKET sock, **const** char *buf, int nbytes, int flags, **const** **struct** sockadr *to, int addrlen); 

Windows

Linux 和 Windows 下的 sendto() 函数类似,下面是详细参数说明:

  • sock:用于传输 UDP 数据的套接字;
  • buf:保存待传输数据的缓冲区地址;
  • nbytes:带传输数据的长度(以字节计);
  • flags:可选项参数,若没有可传递 0;
  • to:存有目标地址信息的 sockaddr 结构体变量的地址;
  • addrlen:传递给参数 to 的地址值结构体变量的长度。

UDP 发送函数 sendto() 与TCP发送函数 write()/send() 的最大区别在于,sendto() 函数需要向他传递目标地址信息。

接收数据使用 recvfrom() 函数:

1.  ssize_t recvfrom(int sock, void *buf, size_t nbytes, int flags, **struct** sockadr *from, socklen_t *addrlen); //Linux

2.  int recvfrom(SOCKET sock, char *buf, int nbytes, int flags, **const** **struct** sockaddr *from, int *addrlen); 

由于 UDP 数据的发送端不定,所以 recvfrom() 函数定义为可接收发送端信息的形式,具体参数如下:

  • sock:用于接收 UDP 数据的套接字;
  • buf:保存接收数据的缓冲区地址;
  • nbytes:可接收的最大字节数(不能超过 buf 缓冲区的大小);
  • flags:可选项参数,若没有可传递 0;
  • from:存有发送端地址信息的 sockaddr 结构体变量的地址;
  • addrlen:保存参数 from 的结构体变量长度的变量地址值。

基于UDP的回声服务器端/客户端

下面结合之前的内容实现回声客户端。需要注意的是,UDP 不同于 TCP,不存在请求连接和受理过程,因此在某种意义上无法明确区分服务器端和客户端,只是因为其提供服务而称为服务器端,希望各位读者不要误解。

☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆

TCP和UDP总结

TCP:传输控制协议

  1. 建立连接,形成数据传输的通道
  2. 在连接中进行大量的数据传输(数据大小不受限制)
  3. 通过三次握手完成连接,是可靠的协议,安全送达必须建立连接,效率会稍低

UDP:用户数据报协议

  1. 将数据及源和目的封装在数据包中,不需要建立连接
  2. 因为是无需建立连接所以是不可靠的
  3. 每个数据报的大小限制在64k之内
  4. 不需要建立连接因此速度快

TCP与UDP的优劣特点:

TCP 是面向连接的传输协议,建立连接时要经过三次握手,断开连接时要经过四次握手,中间传输数据时也要回复 ACK 包确认,多种机制保证了数据能够正确到达,不会丢失或出错。

UDP 是非连接的传输协议,没有建立连接和断开连接的过程,它只是简单地把数据丢到网络中,也不需要 ACK 包确认。

UDP 传输数据就好像我们邮寄包裹,邮寄前需要填好寄件人和收件人地址,之后送到快递公司即可,但包裹是否正确送达、是否损坏我们无法得知,也无法保证。UDP 协议也是如此,它只管把数据包发送到网络,然后就不管了,如果数据丢失或损坏,发送端是无法知道的,当然也不会重发。

既然如此,TCP 应该是更加优质的传输协议吧?

如果只考虑可靠性,TCP 的确比 UDP 好。但 UDP 在结构上比 TCP 更加简洁,不会发送 ACK 的应答消息,也不会给数据包分配 Seq 序号,所以 UDP 的传输效率有时会比 TCP 高出很多,编程中实现 UDP 也比 TCP 简单。

UDP 的可靠性虽然比不上TCP,但也不会像想象中那么频繁地发生数据损毁,在更加重视传输效率而非可靠性的情况下,UDP 是一种很好的选择。比如视频通信或音频通信,就非常适合采用 UDP 协议;通信时数据必须高效传输才不会产生“卡顿”现象,用户体验才更加流畅,如果丢失几个数据包,视频画面可能会出现“雪花”,音频可能会夹带一些杂音,这些都是无妨的。

与 UDP 相比,TCP 的生命在于流控制,这保证了数据传输的正确性。

最后需要说明的是:TCP 的速度无法超越 UDP,但在收发某些类型的数据时有可能接近 UDP。例如,每次交换的数据量越大,TCP 的传输速率就越接近于 UDP。

☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆

日常开发中Socket的应用

示例代码 : Linux 下的 socket 程序

☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆

socket的基本使用

Linux 下的代码

server.cpp 是服务器端代码,client.cpp 是客户端代码,要实现的功能是:客户端从服务器读取一个字符串并打印出来。

服务器端代码 server.cpp:

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(){

  //创建套接字

 int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

 //将套接字和IP、端口绑定

 struct sockaddr_in serv_addr;

 memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充

 serv_addr.sin_family = AF_INET; //使用IPv4地址

 serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址

 serv_addr.sin_port = htons(1234); //端口

 bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

 //进入监听状态,等待用户发起请求

 listen(serv_sock, 20);

 //接收客户端请求

 struct sockaddr_in clnt_addr;

 socklen_t clnt_addr_size = sizeof(clnt_addr);

 int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);

 //向客户端发送数据

 char str[] = "http://c.biancheng.net/socket/";

 write(clnt_sock, str, sizeof(str));

 //关闭套接字

 close(clnt_sock);

 close(serv_sock);

 return 0;

}

客户端代码 client.cpp:

#include 
#include 
#include 
#include 
#include 
#include 

int main(){

   //创建套接字

 int sock = socket(AF_INET, SOCK_STREAM, 0);

 //向服务器(特定的IP和端口)发起请求

 struct sockaddr_in serv_addr;

 memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充

 serv_addr.sin_family = AF_INET; //使用IPv4地址

 serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址

 serv_addr.sin_port = htons(1234); //端口

 connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

 //读取服务器传回的数据

 char buffer[40];

 read(sock, buffer, sizeof(buffer)-1);

 printf("Message form server: %s\n", buffer);

 //关闭套接字

 close(sock);

 return 0;

}

启动一个终端(Shell),先编译 server.cpp 并运行:

[admin@localhost ~]$ g++ server.cpp -o server
[admin@localhost ~]$ ./server

等待请求的到来

正常情况下,程序运行到 accept() 函数就会被阻塞,等待客户端发起请求。

接下再启动一个终端,编译 client.cpp 并运行:

[admin@localhost ~]$ g++ client.cpp -o client
[admin@localhost ~]$ ./client

client 接收到从 server发送过来的字符串就运行结束了,同时,server 完成发送字符串的任务也运行结束了。大家可以通过两个打开的终端来观察。

client 运行后,通过 connect() 函数向 server 发起请求,处于监听状态的 server 被激活,执行 accept() 函数,接受客户端的请求,然后执行 write() 函数向 client 传回数据。client 接收到传回的数据后,connect() 就运行结束了,然后使用 read() 将数据读取出来。

server 只接受一次 client 请求,当 server 向 client 传回数据后,程序就运行结束了。如果想再次接收到服务器的数据,必须再次运行 server,所以这是一个非常简陋的 socket 程序,不能够一直接受客户端的请求。

源码解析

  1. 先说一下 server.cpp 中的代码。

通过 socket() 函数创建了一个套接字,参数 AF_INET 表示使用 IPv4 地址,SOCK_STREAM 表示使用面向连接的套接字,IPPROTO_TCP 表示使用 TCP 协议。在 Linux 中,socket 也是一种文件,有文件描述符,可以使用 write() / read() 函数进行 I/O 操作,这一点已在《socket是什么》中进行了讲解。

通过 bind() 函数将套接字 serv_sock 与特定的 IP 地址和端口绑定,IP 地址和端口都保存在 sockaddr_in 结构体中。

socket() 函数确定了套接字的各种属性,bind() 函数让套接字与特定的IP地址和端口对应起来,这样客户端才能连接到该套接字。

套接字处于被动监听状态。所谓被动监听,是指套接字一直处于“睡眠”中,直到客户端发起请求才会被“唤醒”。

accept() 函数用来接收客户端的请求。程序一旦执行到 accept() 就会被阻塞(暂停运行),直到客户端发起请求。

write() 函数用来向套接字文件中写入数据,也就是向客户端发送数据。

和普通文件一样,socket 在使用完毕后也要用 close() 关闭。

  1. 再说一下 client.cpp 中的代码。client.cpp 中的代码和 server.cpp 中有一些区别。

通过 connect() 向服务器发起请求,服务器的IP地址和端口号保存在 sockaddr_in 结构体中。直到服务器传回数据后,connect() 才运行结束。

通过 read() 从套接字文件中读取数据。

Windows 下的代码

服务器端 server.cpp

#include 
#include 
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
#define BUF_SIZE 100

int main(){

 WSADATA wsaData;

 WSAStartup( MAKEWORD(2, 2), &wsaData);

 //创建套接字

 SOCKET sock = [socket](http://c.biancheng.net/socket/)(AF_INET, SOCK_DGRAM, 0);

 //绑定套接字

 **struct** sockaddr_in servAddr;

 memset(&servAddr, 0, **sizeof**(servAddr)); //每个字节都用0填充

 servAddr.sin_family = PF_INET; //使用IPv4地址

 servAddr.sin_addr.s_addr = htonl(INADDR_ANY); //自动获取IP地址

 servAddr.sin_port = htons(1234); //端口

 bind(sock, (SOCKADDR*)&servAddr, **sizeof**(SOCKADDR));

 //接收客户端请求

 SOCKADDR clntAddr; //客户端地址信息

 int nSize = **sizeof**(SOCKADDR);

 char buffer[BUF_SIZE]; //缓冲区

 **while**(1){

 int strLen = recvfrom(sock, buffer, BUF_SIZE, 0, &clntAddr, &nSize);

 sendto(sock, buffer, strLen, 0, &clntAddr, nSize);

 }

 closesocket(sock);

 WSACleanup();

 **return** 0;

}

代码说明:

  1. 代码在创建套接字时,向 socket() 第二个参数传递 SOCK_DGRAM,以指明使用 UDP 协议。
  2. 使用htonl(INADDR_ANY)来自动获取 IP 地址。

利用常数 INADDR_ANY 自动获取 IP 地址有一个明显的好处,就是当软件安装到其他服务器或者服务器 IP 地址改变时,不用再更改源码重新编译,也不用在启动软件时手动输入。而且,如果一台计算机中已分配多个 IP 地址(例如路由器),那么只要端口号一致,就可以从不同的 IP 地址接收数据。所以,服务器中优先考虑使用 INADDR_ANY;而客户端中除非带有一部分服务器功能,否则不会采用。

客户端 client.cpp:

#include 
#include 
#pragma comment(lib, "ws2_32.lib") //加载 ws2_32.dll
#define BUF_SIZE 100

int main(){

  //初始化DLL

 WSADATA wsaData;

 WSAStartup(MAKEWORD(2, 2), &wsaData);

 //创建套接字

 SOCKET sock = socket(PF_INET, SOCK_DGRAM, 0);

 //服务器地址信息

 **struct** sockaddr_in servAddr;

 memset(&servAddr, 0, **sizeof**(servAddr)); //每个字节都用0填充

 servAddr.sin_family = PF_INET;

 servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");

 servAddr.sin_port = htons(1234);

 //不断获取用户输入并发送给服务器,然后接受服务器数据

 **struct** sockaddr fromAddr;

 int addrLen = **sizeof**(fromAddr);

 **while**(1){

 char buffer[BUF_SIZE] = {0};

 printf("Input a string: ");

 gets(buffer);

 sendto(sock, buffer, strlen(buffer), 0, (**struct** sockaddr*)&servAddr, **sizeof**(servAddr));

 int strLen = recvfrom(sock, buffer, BUF_SIZE, 0, &fromAddr, &addrLen);

 buffer[strLen] = 0;

 printf("Message form server: %s\n", buffer);

 }

 closesocket(sock);

 WSACleanup();

 **return** 0;

}

从代码中可以看出,server.cpp 中没有使用 listen() 函数,client.cpp 中也没有使用 connect() 函数,因为 UDP 不需要连接。

iOS下的代码

常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。

socket调用库函数主要有:

创建套接字
Socket(af,type,protocol)

建立地址和套接字的联系
bind(sockid, local addr, addrlen)

服务器端侦听客户端的请求 
listen(Sockid ,quenlen)

建立服务器/客户端的连接 (面向连接TCP)

客户端请求连接
Connect(sockid, destaddr, addrlen)

服务器端等待从编号为Sockid的Socket上接收客户连接请求
newsockid = accept(Sockid,Clientaddr, paddrlen)

发送/接收数据

  • 面向连接:
send(sockid, buff, bufflen)
recv( )
  • 面向无连接:
sendto(sockid,buff,…,addrlen)
recvfrom( )
  • 释放套接字
close(sockid)

tcpsocket的具体实现

服务器的工作流程:首先调用socket函数创建一个Socket,然后调用bind函数将其与本机地址以及一个本地端口号绑定,然后调用listen在相应的socket上监听,当accpet接收到一个连接服务请求时,将生成一个新的socket。服务器显示该客户机的IP地址,并通过新的socket向客户端发送字符串” hi,I am server!”。最后关闭该socket。

☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆

iOS socket服务端代码:

UDP/IP应用编程接口(API)

  • 服务器的工作流程:首先调用socket函数创建一个Socket,然后调用bind函数将其与本机
  • 地址以及一个本地端口号绑定,接收到一个客户端时,服务器显示该客户端的IP地址,并将字串
  • 返回给客户端。
#include
#include
#include
#include
#include
#include
#include
#import 

int main(int argc,char **argv)

{

    int ser_sockfd;

 int len;

 //int addrlen;

 socklen_t addrlen;

 char seraddr[100];

 struct sockaddr_in ser_addr;

  /*建立socket*/

 ser_sockfd = socket(AF_INET,SOCK_DGRAM,0);

 if(ser_sockfd < 0)

 {

 printf("I cannot socket success\n");

 return 1;

 }

 /*填写sockaddr_in 结构*/

 addrlen = sizeof(struct sockaddr_in);

 bzero(&ser_addr, addrlen);

 ser_addr.sin_family = AF_INET;

 ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);

 ser_addr.sin_port = htons(1024);

 /*绑定客户端*/

 if(bind(ser_sockfd,(struct sockaddr *)&ser_addr,addrlen)<0)

 {

 printf("connect");

 return 1;

 }

 while(1)

 {

 bzero(seraddr,sizeof(seraddr));

 len=recvfrom(ser_sockfd,seraddr,sizeof(seraddr),0,(struct sockaddr*)&ser_addr,&addrlen);

 /*显示client端的网络地址*/

 printf("receive from %s\n",inet_ntoa(ser_addr.sin_addr));

 /*显示客户端发来的字串*/

 printf("recevce:%s",seraddr);

 /*将字串返回给client端*/

 sendto(ser_sockfd, seraddr,len,0,(struct sockaddr*)&ser_addr,addrlen);

 }

}

客户端的工作流程:首先调用socket函数创建一个Socket,填写服务器地址及端口号,从标准输入设备中取得字符串,将字符串传送给服务器端,并接收服务器端返回的字符串。最后关闭该socket。

☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆

UDP/IP应用编程接口(API)

  • 客户端的工作流程:首先调用socket函数创建一个Socket,填写服务器地址及端口号,
  • 从标准输入设备中取得字符串,将字符串传送给服务器端,并接收服务器端返回的字
  • 符串。最后关闭该socket。
#include
#include
#include
#include
#include
#include
#include
#include 
#import 

int GetServerAddr(char * addrname)
{

 printf("please input server addr:");

 scanf("%s",addrname);

 return 1;

}

int main(int argc,char **argv)

{

  int cli_sockfd;

 int len;

 socklen_t addrlen;

 char seraddr[14];

 struct sockaddr_in cli_addr;

 char buffer[256];

 GetServerAddr(seraddr);

  /* 建立socket*/

 cli_sockfd = socket(AF_INET,SOCK_DGRAM,0);

 if(cli_sockfd < 0)

 {

 printf("I cannot socket success\n");

 return 1;

 }

 /* 填写sockaddr_in */

 addrlen = sizeof(struct sockaddr_in);

 bzero(&cli_addr, addrlen);

 cli_addr.sin_family = AF_INET;

 cli_addr.sin_addr.s_addr = inet_addr(seraddr);

 //cli_addr.sin_addr.s_addr = htonl(INADDR_ANY);

 cli_addr.sin_port = htons(1024);

 bzero(buffer,sizeof(buffer));

 /* 从标准输入设备取得字符串*/

 len=read(STDIN_FILENO,buffer,sizeof(buffer));

 /* 将字符串传送给server端*/

 sendto(cli_sockfd,buffer,len,0,(struct sockaddr*)&cli_addr,addrlen);

 /* 接收server端返回的字符串*/

 len=recvfrom(cli_sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&cli_addr,&addrlen);

 //printf("receive from %s\n",inet_ntoa(cli_addr.sin_addr));

 printf("receive: %s",buffer);

 close(cli_sockfd);

}

☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆

文章到此结束,喜欢的留颗星星!

你可能感兴趣的:(Socket开发实战)