Socket编程入门
socket基本知识
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。
Java Socket的通信时序图如下。
Java Socket的数据通信模型如下。
Java编程
- 通信步骤
Server端 | Client端 |
|
|
- socket简单对话
·Server端
public class TestServer { public static final String datePattern = "yyyy-MM-dd HH:mm:ss SSS"; public static final SimpleDateFormat dateFormat = new SimpleDateFormat(datePattern);
public static void main(String[] args) throws IOException { //1.创建一个server socket服务 ServerSocket serverSocket = new ServerSocket(); //2.绑定端口 InetSocketAddress address = new InetSocketAddress("localhost", 18824); serverSocket.bind(address); //3.等待和接收端口的通信请求,返回的是一个socket PrintConsoleMsg("等待连接..."); Socket socket = serverSocket.accept(); PrintConsoleMsg("连接成功!");
//服务端的输入与输出 BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter writer = new PrintWriter(socket.getOutputStream(), true); //true表示autoflush
//获取键盘输入 BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in));
while(true) { if(reader.ready()) { //捕获client的socket发来的消息 PrintClientMsg(reader.readLine()); } if(keyboard.ready()) { //捕获当前server的键盘输入 String content = keyboard.readLine(); //打印在server的屏幕 PrintConsoleMsg(content); //发送到client writer.println(content); } }
}
public static void PrintConsoleMsg(String msg) { System.out.println("Server:\t" + dateFormat.format(new Date()) + "\t" + msg); }
public static void PrintClientMsg(String msg) { System.out.println("Client:\t" + dateFormat.format(new Date()) + "\t" + msg); } } |
·Client端
public class TestClient { public static final String datePattern = "yyyy-MM-dd HH:mm:ss SSS"; public static final SimpleDateFormat dateFormat = new SimpleDateFormat(datePattern); public static void main(String[] args) throws UnknownHostException, IOException {
//1.创建一个socket Socket socket = new Socket(); //2.连接server的IP:端口 InetSocketAddress address = new InetSocketAddress("localhost", 18824); socket.connect(address); //3.client socket的输入流和输出流 BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter writer = new PrintWriter(socket.getOutputStream(),true); //当前client的键盘输入流 BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in));
while(true) { if(reader.ready()) { PrintServerMsg(reader.readLine()); } if(keyboard.ready()) { String content = keyboard.readLine(); //打印在client的console PrintConsoleMsg(content); //发送给server writer.println(content); } } }
public static void PrintConsoleMsg(String msg) { System.out.println("Client:\t" + dateFormat.format(new Date()) + "\t" + msg); }
public static void PrintServerMsg(String msg) { System.out.println("Server:\t" + dateFormat.format(new Date()) + "\t" + msg); } } |
·运行结果
打开2个power shell分别作为server和client端。
- 运行server端
- 运行client进行连接
- client输入信息,模拟通信
- server输入回复信息
- client输入回复信息
- 多客户端与单服务器
·server端
public class MyServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(); InetSocketAddress address = new InetSocketAddress("localhost", 18824); serverSocket.bind(address);
while(true) { Socket socket = serverSocket.accept(); System.out.println("Client " + socket.getPort() + "连接成功!"); new Thread(new SocketHandler(socket)).start(); } } }
class SocketHandler implements Runnable{ public static final String datePattern = "yyyy-MM-dd HH:mm:ss SSS"; public static final SimpleDateFormat dateFormat = new SimpleDateFormat(datePattern); private Socket socket;
public SocketHandler(Socket socket) { // TODO Auto-generated constructor stub this.socket = socket; }
@Override public void run() { // TODO Auto-generated method stub try { BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter writer = new PrintWriter(socket.getOutputStream(),true); BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in)); boolean flag = true; while(flag) { if(reader.ready()) { String content = reader.readLine(); PrintClientMsg(content, socket.getPort()); //getPort获取client的端口 if(content == "exit") { flag=false; socket.close(); } } if(keyboard.ready()) { String content = keyboard.readLine(); writer.println(content); PrintServerMsg(content); } } } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } }
public static void PrintServerMsg(String msg) { System.out.println("Server:\t" + dateFormat.format(new Date()) + "\t" + msg); } public static void PrintClientMsg(String msg, int port) { System.out.println("Client " + port + ":\t" + dateFormat.format(new Date()) + "\t" + msg); } } |
- client端
public class MyClient { public static final String datePattern = "yyyy-MM-dd HH:mm:ss SSS"; public static final SimpleDateFormat dateFormat = new SimpleDateFormat(datePattern); public static void main(String[] args) throws UnknownHostException, IOException { Socket socket = new Socket("localhost", 18824); BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter writer = new PrintWriter(socket.getOutputStream(),true); BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in)); while(true) { if(reader.ready()) { String content = reader.readLine(); PrintServerMsg(content); }
if(keyboard.ready()) { String content = keyboard.readLine(); writer.println(content); PrintClientMsg(content); if(content=="exit") { reader.close(); writer.close(); keyboard.close(); socket.close(); } } } }
public static void PrintServerMsg(String msg) { System.out.println("Server:\t" + dateFormat.format(new Date()) + "\t" + msg); } public static void PrintClientMsg(String msg) { System.out.println("Client:\t" + dateFormat.format(new Date()) + "\t" + msg); }
} |
- 单个client的运行结果
- 2个client的运行结果
- 结果分析
对于单个client的情况:client与server能够双向通信。
对于k个(k>=2)client的情况:每个client都能够发信息到server。server也能发信息到每一个client,但是对于是哪一个client接收却无法确定(可能跟多线程管理有关)。
C++编程
- 常用函数
函数 | 说明 |
uint16_t htons(uint16_t hostshort); | htons是将整型变量从主机字节顺序转变成网络字节顺序, 就是整数在地址空间存储方式变为高位字节存放在内存的低地址处。(小端->大端) |
int socket (int domain, int type, int protocol); | 建立一个协议族为domain、协议类型为type、协议编号为protocol的套接字文件描述符。如果函数调用成功,会返回一个标识这个套接字的文件描述符,失败的时候返回-1。 |
int bind (SOCKET socket, struct sockaddr* address, socklen_t address_len); | socket:是一个套接字描述符。 address:是一个sockaddr结构指针,该结构中包含了要结合的地址和端口号。(sockaddr与sockaddr_in等价) address_len:确定address缓冲区的长度。 返回值:如果函数执行成功,返回值为0,否则为SOCKET_ERROR。 |
int listen(int fd, int backlog); | listen函数使用主动连接套接字变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。 listen函数一般在调用bind之后-调用accept之前调用。 fd 一个已绑定未被连接的套接字描述符 backlog 连接请求队列(queue of pending connections) 的最大长度(一般由2到4)。 无错误,返回0,否则-1 |
int recv (SOCKET socket, char FAR* buf, int len, int flags); | socket:一个标识已连接套接口的描述字。 buf:用于接收数据的缓冲区。 len:缓冲区长度。 flags:指定调用方式。取值:MSG_PEEK 查看当前数据,数据将被复制到缓冲区中,但并不从输入队列中删除;MSG_OOB 处理带外数据。 返回值: 若无错误发生,recv()返回读入的字节数。如果连接已中止(另一端终止了连接),返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。 |
int accept(int sockfd, void *addr, int *addrlen); | sockfd: server端的socket fd addr和addrlen:client的sockaddr_in 成功返回一个新的套接字描述符,失败返回-1。 |
ssize_t send (int s, const void *msg, size_t len, int flags); | s指定发送端套接字描述符; 第二个参数指明一个存放应用程式要发送数据的缓冲区; 第三个参数指明实际要发送的数据的字符数; 第四个参数一般置0。 |
in_addr_t inet_addr (const char* strptr); | 将一个点分十进制的IP转换成一个长整数型数(u_long类型) |
- domain的含义
名称 | 含义 | 名称 | 含义 |
PF_UNIX,PF_LOCAL | 本地通信 | PF_X25 | ITU-T X25 / ISO-8208协议 |
AF_INET, PF_INET | IPv4 Internet协议 | PF_AX25 | Amateur radio AX.25 |
PF_INET6 | IPv6 Internet协议 | PF_ATMPVC | 原始ATM PVC访问 |
PF_IPX | IPX-Novell协议 | PF_APPLETALK | Appletalk |
PF_NETLINK | 内核用户界面设备 | PF_PACKET | 底层包访问 |
- type含义
名称 | 含义 |
SOCK_STREAM | Tcp连接,提供序列化的、可靠的、双向连接的字节流。支持带外数据传输 |
SOCK_DGRAM | 支持UDP连接(无连接状态的消息) |
SOCK_SEQPACKET | 序列化包,提供一个序列化的、可靠的、双向的基本连接的数据传输通道,数据长度定常。每次调用读系统调用时数据需要将全部数据读出 |
SOCK_RAW | RAW类型,提供原始网络协议访问 |
SOCK_RDM | 提供可靠的数据报文,不过可能数据会有乱序 |
SOCK_PACKET | 这是一个专用类型,不能呢过在通用程序中使用 |
- protocol含义
函数socket()的第3个参数protocol用于制定某个协议的特定类型,即type类型中的某个类型。通常某协议中只有一种特定类型,这样protocol参数仅能设置为0;但是有些协议有多种特定的类型,就需要设置这个参数来选择特定的类型。
·SOCK_STREAM的套接字表示一个双向的字节流,与管道类似。流式的套接字在进行数据收发之前必须已经连接,连接使用connect()函数进行。一旦连接,可以使用read()或者write()函数进行数据的传输。流式通信方式保证数据不会丢失或者重复接收,当数据在一段时间内任然没有接受完毕,可以将这个连接人为已经死掉。
·SOCK_DGRAM和SOCK_RAW 这个两种套接字可以使用函数sendto()来发送数据,使用recvfrom()函数接受数据,recvfrom()接受来自制定IP地址的发送方的数据。
·SOCK_PACKET是一种专用的数据包,它直接从设备驱动接受数据。
- 通信步骤
- 名词解析
htonl()--"Host to Network Long"
ntohl()--"Network to Host Long"
htons()--"Host to Network Short"
ntohs()--"Network to Host Short"
AF--Address Family
PF—Procotol Family
- socket简单对话
实现功能:server和client"一问一答"的对话。
·helper.c
#include <time.h> void print_time() { time_t t; struct tm *lt; time(&t); lt = localtime(&t); printf(" %d/%d/%d %d:%d:%d ", lt->tm_year+1900, lt->tm_mon, lt->tm_mday, lt->tm_hour, lt->tm_min, lt->tm_sec); } |
·server端
#define MYPORT 8887 #define QUEUE 20 #define BUFFER_SIZE 1024 int main() { //定义socket fd int server_socket_fd = socket(AF_INET, SOCK_STREAM, 0); //定义sockaddr in //'sin' means socket input struct sockaddr_in server_sinaddr; server_sinaddr.sin_family = AF_INET; //协议 server_sinaddr.sin_port = htons(MYPORT);//端口 server_sinaddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP
//bind success? //bind server_socket_fd with server_sinaddr if(bind(server_socket_fd, (struct sockaddr *)&server_sinaddr, sizeof(server_sinaddr)) == -1) { perror("bind error\n"); exit(-1); } printf("bind success...\n"); //listen success? //waiting for connecting //QUEUE means 20 requests is permitted if(listen(server_socket_fd, QUEUE) == -1) { perror("listen error\n"); exit(-1); } printf("waitting connect request...\n"); //client char buffer[BUFFER_SIZE] = { 0 }; struct sockaddr_in client_sinaddr; socklen_t length = sizeof(client_sinaddr);
//client_socket_fd 是一个已连接socket fd int client_socket_fd = accept(server_socket_fd, (struct sockaddr*)&client_sinaddr, &length); if(client_socket_fd < 0) { perror("connect error\n"); exit(-1); }
print_time(); printf(" : connect success\n");
char reply[BUFFER_SIZE]; while(1) { memset(buffer, 0, sizeof(buffer)); int len = recv(client_socket_fd, buffer, sizeof(buffer), 0); if(strcmp(buffer, "exit\n") == 0 || strcmp(buffer,"exit") == 0) { print_time(); printf("exit...\n"); break; } printf("Client\t"); print_time(); fputs(buffer, stdout); //send(client_socket_fd, buffer, len, 0);//此处应该是向client返回同样的数据 fgets(reply, sizeof(reply), stdin); send(client_socket_fd, reply, strlen(reply), 0); } close(client_socket_fd); close(server_socket_fd); return 0; } |
·client端
#define MYPORT 8887 #define BUFFER_SIZE 1024 int main() { int client_socket_fd = socket(AF_INET, SOCK_STREAM, 0);
//sock addr struct sockaddr_in client_sinaddr; client_sinaddr.sin_family = AF_INET; client_sinaddr.sin_port = htons(MYPORT); client_sinaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//connect to server if(connect(client_socket_fd, (struct sockaddr*)&client_sinaddr, sizeof(client_sinaddr)) < 0) { perror("connect error\n"); exit(-1); } print_time(); printf(" connect to server successfully...\n"); char send_buf[BUFFER_SIZE]; char recv_buf[BUFFER_SIZE]; while(fgets(send_buf, sizeof(send_buf), stdin) != NULL) { send(client_socket_fd, send_buf, strlen(send_buf), 0); printf("Client\t"); print_time(); fputs(send_buf, stdout); if(strcmp(send_buf, "exit\n") == 0 || strcmp(send_buf, "exit") == 0) { print_time(); printf("exit...\n"); break; } recv(client_socket_fd, recv_buf, sizeof(recv_buf), 0); printf("Server\t"); print_time(); fputs(recv_buf, stdout); memset(send_buf, 0, sizeof(send_buf)); memset(recv_buf, 0, sizeof(recv_buf)); } close(client_socket_fd); return 0; } |
·运行结果
连接成功
client向server发送消息
server回复client
后续1
后续2
输入exit
- 参考文献
https://blog.csdn.net/luanlouis/article/details/19974999