Socket 编程(基于Linux)
Socket独立于具体协议的网络编程接口,在ISO模型中,主要位于会话层和传输层之间;在通用的计算机网络五层模型中,主要位于应用层和传输层之间。
Linux Socket
- 基本上就是BSD Socket
- 需要使用的头文件
- 数据类型:#include <sys/types.h>
- 函数定义:#include <sys/socket.h>
Socket类型
套接字是一种通信机制,通信两方的一种约定,用套接字中的相关函数来完成通信过程。根据传输内容分为流式套接字、数据报套接字、原始套接字;根据使用方式分为主动套接字和被动套接字。
流式套接字(SOCK_STREAM)
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。(面向TCP)
数据报套接字(SOCK_DGRAM)
提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。(面向UDP)
原始套接字(SOCK_RAW)
可以对较低层次协议,如IP、ICMP直接访问。
主动套接字和被动套接字
创建方式相同,使用方式不同
- 等待传入连接的套接字——被动,如服务器套接字
- 发起连接的套接字——主动,如客户套接字
指明端点地址:创建时不指定,使用时指明:
- TCP/IP需要指明协议端口号和IP地址
- TCP/IP协议族:PF_INET
- TCP/IP的地址族:AF_INET
Linux Socket API函数
- socket 创建套接字
- connect 建立连接
- bind 绑定本机端口
- listen 监听端口
- accept 接受连接
- recv, recvfrom 数据接收
- send, sendto 数据发送
- close, shutdown 关闭套接字
TCP下通信调用Linux Socket API流程如下:
UDP下通信调用Linux Socket API流程如下:
Linux Socket API函数详解
socket函数
int socket( int domain, int type, int protocol)
功能:创建一个新的套接字,返回套接字描述符
参数说明:
- domain:域类型,指明使用的协议栈,如TCP/IP使用的是 PF_INET
- type: 指明需要的服务类型, 如:
- SOCK_DGRAM: 数据报服务,UDP协议
- SOCK_STREAM: 流服务,TCP协议
- protocol:一般都取0
举例:s=socket(PF_INET,SOCK_STREAM,0)
connect函数
int connect(int sockfd,struct sockaddr *server_addr,int sockaddr_len)
功能: 同远程服务器建立主动连接,成功时返回0,若连接失败返回-1。
参数说明:
- Sockfd:套接字描述符,指明创建连接的套接字
- Server_addr:指明远程端点:IP地址和端口号
- sockaddr_len :地址长度
bind函数
int bind(int sockfd,struct sockaddr * my_addr,int addrlen)
功能:为套接字指明一个本地端点地址TCP/IP协议使用sockaddr_in结构,包含IP地址和端口号,服务器使用它来指明熟知的端口号,然后等待连接
参数说明:
- Sockfd:套接字描述符,指明创建连接的套接字
- my_addr:本地地址,IP地址和端口号
- addrlen :地址长度
举例:bind(sockfd, (struct sockaddr *)&address, sizeof(address));
listen函数(TCP)
int listen(int sockfd,int input_queue_size)
功能:面向连接的服务器使用它将一个套接字置为被动模式,并准备接收传入连接。用于服务器,指明某个套接字连接是被动的
参数说明:
- Sockfd:套接字描述符,指明创建连接的套接字
- input_queue_size:该套接字使用的队列长度,指定在请求队列中允许的最大请求数
举例:listen(sockfd,20)
accept函数(TCP)
int accept(int sockfd, void *addr, int *addrlen);
功能:获取传入连接请求,返回新的连接的套接字描述符。为每个新的连接请求创建了一个新的套接字,服务器只对新的连接使用该套接字,原来的监听套接字接受其他的连接请求。新的连接上传输数据使用新的套接字,使用完毕,服务器将关闭这个套接字。
参数说明:
- sockfd:套接字描述符,指明正在监听的套接字
- addr:提出连接请求的主机地址
- addrlen:地址长度
举例:new_sockfd = accept(sockfd, (struct sockaddr *)&address, &addrlen);
UDP下接收与发送
sendto函数
int sendto(int sockfd, const void * data, int data_len, unsigned int flags, struct sockaddr *remaddr,int remaddr_len)
功能:基于UDP发送数据报,返回实际发送的数据长度,出错时返回-1
参数说明:
- sockfd:套接字描述符
- data:指向要发送数据的指针
- data_len:数据长度
- flags:一直为0
- remaddr:远端地址:IP地址和端口号
- remaddr_len :地址长度
举例:sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&address, sizeof(address));
recvfrom函数
int recvfrom(int sockfd, void *buf, int buf_len,unsigned int flags,struct sockaddr *from,int *fromlen);
功能:从UDP接收数据,返回实际接收的字节数,失败时返回-1
参数说明:
- sockfd:套接字描述符
- buf:指向内存块的指针
- buf_len:内存块大小,以字节为单位
- flags:一般为0
- from:远端的地址,IP地址和端口号
- fromlen:远端地址长度
举例:recvfrom(sockfd,buf,8192,0, ,(struct sockaddr *)&address, &fromlen);
TCP下接收与发送
send函数
int send(int sockfd, const void * data, int data_len, unsigned int flags)
功能:在TCP连接上发送数据,返回成功传送数据的长度,出错时返回-1。send会将外发数据复制到OS内核中
参数说明:
- sockfd:套接字描述符
- data:指向要发送数据的指针
- data_len:数据长度
- flags:一直为0
举例:send(s,req,strlen(req),0);
recv函数
int recv(int sockfd, void *buf, int buf_len,unsigned int flags);
功能:从TCP接收数据,返回实际接收的数据长度,出错时返回-1。服务器使用其接收客户请求,客户使用它接受服务器的应答。如果没有数据,将阻塞,如果收到的数据大于缓存的大小,多余的数据将丢弃。
参数说明:
- sockfd:套接字描述符
- Buf:指向内存块的指针
- Buf_len:内存块大小,以字节为单位
- flags:一般为0
举例:recv(sockfd,buf,8192,0)
close函数
close(int sockfd);
功能:撤销套接字。如果只有一个进程使用,立即终止连接并撤销该套接字,如果多个进程共享该套接字,将引用数减一,如果引用数降到零,则撤销它。
参数说明:
- Sockfd:套接字描述符
举例:close(socket_descriptor)
转换函数
1.IP地址转换函数
- inet_addr() 点分十进制数表示的IP地址转换为网络字节序的IP地址
- inet_ntoa() 网络字节序的IP地址转换为点分十进制数表示的IP地址
2.字节排序函数 - htonl 4字节主机字节序转换为网络字节序
- ntohl 4字节网络字节序转换为主机字节序
- htons 2字节主机字节序转换为网络字节序
- ntohs 2字节网络字节序转换为主机字节序
域名解析等相关函数
- gethostname 获得主机名
- getpeername 获得与套接口相连的远程协议地址
- getsockname 获得套接口本地协议地址
- gethostbyname 根据主机名取得主机信息
- gethostbyaddr 根据主机地址取得主机信息
- getprotobyname 根据协议名取得主机协议信息
- getprotobynumber 根据协议号取得主机协议信息
- getservbyname 根据服务名取得相关服务信息
- getservbyport 根据端口号取得相关服务信息
- getsockopt/setsockopt 获取/设置一个套接口选项
- ioctlsocket 设置套接口的工作方式
java下的TCP socket编程
java.net.Socket继承于java.lang.Object,有八个构造器,其方法并不多。套接字Socket已经写好封装在java.net.Socket包里。服务器端有特定的ServerSocket方法。
ServerSocket有以下3个属性:
- SO_TIMEOUT:表示等待客户连接的超时时间。一般不设置,会持续等待。
- SO_REUSEADDR:表示是否允许重用服务器所绑定的地址。一般不设置,经我的测试没必要,下面会进行详解。
- SO_RCVBUF:表示接收数据的缓冲区的大小。一般不设置,用系统默认就可以了。
对于Socket类有如下常用方法:
- accept方法用于产生”阻塞”,直到接受到一个连接,并且返回一个客户端的Socket对象实例。”阻塞”是一个术语,它使程序运行暂时”停留”在这个地方,直到一个会话产生,然后程序继续;通常”阻塞”是由循环产生的。
- getInputStream方法获得网络连接输入,同时返回一个InputStream对象实例。
- getOutputStream方法连接的另一端将得到输入,同时返回一个OutputStream对象实例。
注意:其中getInputStream和getOutputStream方法均会产生一个IOException,它必须被捕获,因为它们返回的流对象,通常都会被另一个流对象使用。
java与linux api调用关系
TCP服务器端Socket通信流程
- 创建ServerSocket对象,绑定监听端口。
- 通过accept()方法监听客户端请求。
- 连接建立后,通过输入流读取客户端发送的请求信息。
- 通过输出流向客户端发送响应信息。
- 关闭响应的资源。
TCP客户端通信流程
- 创建Socket对象,指明需要连接的服务器的地址和端口号。
- 连接建立后,通过输出流向服务器发送请求信息。
- 通过输入流获取服务器响应的信息。
- 关闭相应资源。
多线程实现服务器与多客户端之间通信步骤
- 服务器端创建ServerSocket,循环调用accept()等待客户端连接。
- 客户端创建一个socket并请求和服务器端连接。
- 服务器端接受客户端请求,创建socket与该客户建立专线连接。
- 建立连接的两个socket在一个单独的线程上对话。
- 服务器端继续等待新的连接。
多线程下的TCP服务器端:
package socketLearn;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class TcpServerChat {
public static void main(String[] args) throws IOException {
System.out.println("--------Server--------");
ServerSocket server = new ServerSocket(8888);
//1. 指定端口,使用ServerSocket创建服务器
while(true){
//2. 阻塞地等待连接
Socket socket = server.accept();
System.out.println("一个客户端建立了连接");
new Thread(()->{
DataInputStream dis = null;
DataOutputStream dos = null;
BufferedReader br= new BufferedReader(new InputStreamReader(System.in));;
try {
dis = new DataInputStream(socket.getInputStream());
dos = new DataOutputStream(socket.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
boolean isRunning = true;
while(isRunning){
try {
//3. 接收消息
String msg = dis.readUTF();
System.out.println("客户端说:"+msg);
//4.返回消息
String reMsg = br.readLine();
dos.writeUTF(reMsg);
dos.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
try {
dis.close();
socket.close();
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
多线程下的TCP客户端:
package socketLearn;
import java.io.*;
import java.net.Socket;
public class TcpClientChat {
public static void main(String[] args) throws IOException {
System.out.println("--------Client--------");
//1. 建立连接,使用Socket创建客户端
Socket client = new Socket("localhost",8888);
boolean isRunning = true;
BufferedReader console = new BufferedReader((new InputStreamReader(System.in)));
DataOutputStream dos = new DataOutputStream(client.getOutputStream());
DataInputStream dis = new DataInputStream(client.getInputStream());
while(isRunning){
//2. 客户端发送消息
String msg = console.readLine();
dos.writeUTF(msg);
dos.flush();
//3. 获取消息
msg = dis.readUTF();
System.out.println("服务器说:"+msg);
}
dos.close();
dis.close();
client.close();
}
}
正常来说,客户端打开一个输出流,如果不做约定,也不关闭它,那么服务端永远不知道客户端是否发送完消息,那么服务端会一直等待下去,直到读取超时。
- 通过socket关闭
- 关闭客户端socket,服务器端可执行后续操作,弊端也很明显,客户端不能再次发送消息也不能接收消息,而且要再次发送必须再次创建socket连接
- socket关闭输出流
socket.shutdownOutput();
注意不能使用outputStream.close(),若用该法关闭了输出流,那么相应的Socket也将关闭,和直接关闭Socket一个性质。
3. 通过约定的符号
双方约定一个字符或者一个短语,来当做消息发送完成的标识,通常这么做就需要改造读取方法(读取的循环条件)。
4. 通过指定长度
5. 使用DataInputStream、DataOutputStream对socket.getOutputStream,socket.getInputStream进行包装
### UDP Socket编程
* DatagramSocket:用于发送或接收数据包的套接字
* DatagramPacket;数据包
* 不需要利用IO流实现数据的传输每个数据发送单元被统一封装成数据包的方式,发送方将数据包发送到网络中,数据包在网络中去寻找他的目的地。
#### UDP接收方
1、使用DatagramSocket 指定端口创建接收端
2、准备容器封装成DatagramPacket包裹
3、阻塞式接收包裹receive(DatagramPacket p)
4、分析数据byte[] getData();getLength();
5、释放资源
```java
public class UdpServer {
public static void main(String[] args) throws Exception{
System.out.println("接收方启动中...");
DatagramSocket server = new DatagramSocket(9999);
byte[] container = new byte[1024*60];
DatagramPacket packet = new DatagramPacket(container, 0,container.length);
server.receive(packet);
byte[] datas = packet.getData();
int len = packet.getLength();
System.out.println(new String(datas,0,len,"UTF-8"));
server.close();
}
}
UDP发送方
1、使用DatagramSocket 指定端口创建发送端
2、准备数据一定转成字节数组
3、封装成DatagramPacket包裹,需要指定目的地4、发送包裹send(DatagramPacket p)
5、释放资源
public class UdpClient {
public static void main(String[] args) throws Exception{
System.out.println("发送方启动中...");
DatagramSocket client = new DatagramSocket(8888);
String data = "改革春风吹满地";
byte[] datas = data.getBytes();
DatagramPacket packet = new DatagramPacket(datas, 0,datas.length,
new InetSocketAddress("localhost",9999));
client.send(packet);
client.close();
}
}