目录
网络编程是 Linux 中非常重要的一块知识,不管是在工作还是面试中都会遇到,本篇文章将结合实例讲解 socket 网络通信,下面就开始吧!
一、基本概念
socket 通常称为套接字,是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。套接字为上层应用程序提供通信的接口,看下面这张图:

从上图可以看出,socket 实质上是一个抽象层,封装了下面的各种协议,方便了上层应用的调用。
二、原理
服务端:
1. 通过 socket 函数创建套接字描述符;
2. 创建地址,使用 bind 将地址绑定到套接字描述符(此套接字称为监听套接字);
3. 然后,使用 listen 将套接字描述符设置为监听模式,并设置等待连接队列的长度;
4. 如果有连接,使用 accept 取出一个连接,与指定客户端建立连接,并新创建一个套接字用于与客户端通信,此套接字称为已连接套接字;
5. 使用 read/write 函数相互通信;
6. 所有通信完毕,使用 close 函数关闭套接字;
客户端:
1. 通过 socket 创建套接字;
2. 通过 connect 与指定服务器进行连接;
3. 连接成功后通过 read/write 进行通信;
4. 通信结束后通过 close 函数关闭套接字。
不同应用之间通信的过程如下所示:

先介绍下上图中使用到的函数:
2.1 socket
int socket(int domain, int type, int protocol);
该函数返回一个 int 类型的套接字,具有唯一性,可以与 open 函数产生的文件描述符类比。
2.2 bind
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
该函数将地址绑定到 socket 创建的套接上。
2.3 listen
int listen(int sockfd, int backlog);
该函数用于服务端,让服务端可以接收客户端(其它进程)的请求。
2.4 accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
该函数从已完成连接的队列中取出一个连接,同时,返回一个新的(已连接)套接字,称为已连接套接字;而 socket 创建的套接字是监听套接字,监听套接字只有一个,而已连接套接字每一个 TCP 连接就生成一个。
2.5 connect
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
客户端通过 connect 函数与服务端进行连接,如果使用的 TCP 协议,那就是三次握手过程。
2.6 read/recv
ssize_t read(int fd,void *buf,size_t nbyte);
int recv(int sockfd,void *buf,int len,int flags);
该函数负责从 fd/sockfd 中读取内容。
2.7 write/send
ssize_t write(int fd, const void*buf,size_t nbytes);
int send(int sockfd,void *buf,int len,int flags);
write/send 将 buf 中的数据写入文件描述符 fd/sockfd。
2.8 close
int close(int sockfd);
关闭套接字。
以上函数成功返回0,失败返回-1,错误信息存放在 errno 中。
三、实例
服务端代码:
#include <sys/time.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define PORT 3322 // 服务器端口
#define BACKLOG 1
#define MAXRECVLEN 1024
int main(int argc, char *argv[])
{
char buf[MAXRECVLEN];
int listenfd, connectfd; // listendfd 监听套接字 connectfd 已连接套接字
struct sockaddr_in server; // 服务器地址信息
struct sockaddr_in client; // 客户端地址信息
socklen_t addrlen;
// 创建套接字描述符
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket() error. Failed to initiate a socket");
return 0;
}
int opt = SO_REUSEADDR;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
server.sin_addr.s_addr = htonl(INADDR_ANY);
// 将地址 server 绑定到套接字 listenfd 上
if(bind(listenfd, (struct sockaddr *)&server, sizeof(server)) == -1) {
perror("Bind() error.");
return 0;
}
// 将套接字设置为监听模式,队列长度为 1
if(listen(listenfd, BACKLOG) == -1) {
perror("listen() error. \n");
return 0;
}
addrlen = sizeof(client);
while(1) {
// 取出一个客户端请求进行连接
if((connectfd = accept(listenfd,(struct sockaddr *)&client, &addrlen)) == -1) {
printf("accept() error. \n");
break;
}
printf("The client's ip : %s, port %d\n",inet_ntoa(client.sin_addr),htons(client.sin_port));
// 与客户端通信
char str[] = "Hello Client, I'm Server!\n";
while(1) {
int iret = recv(connectfd, buf, MAXRECVLEN, 0);
if(iret > 0) {
printf("%s\n",buf);
} else {
close(connectfd);
break;
}
send(connectfd, str, sizeof(str), 0);
}
}
// 关闭 socket
close(listenfd);
return 0;
}
客户端代码:
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdbool.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#define PORT 3322 //服务器端口
#define MAXDATASIZE 100
int main(int argc, char *argv[])
{
int sockfd, num;
char buf[MAXDATASIZE];
struct hostent *host;
struct sockaddr_in server;
// 参数检查
if (argc != 2) {
printf("Usage: %s <IP Address>\n",argv[0]);
return 0;
}
if((host = gethostbyname(argv[1])) == NULL) {
printf("gethostbyname() error\n");
return 0;
}
// 创建套接字
if((sockfd = socket(AF_INET,SOCK_STREAM, 0))==-1) {
printf("socket() error\n");
return 0;
}
bzero(&server,sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
server.sin_addr = *((struct in_addr *)host->h_addr);
// 与服务器建立连接
if(connect(sockfd, (struct sockaddr *)&server, sizeof(server)) == -1) {
printf("connect() error\n");
return 0;
}
// 与服务器进行通信
char str[] = "Hello Server, I'm Client!\n";
while(true) {
if((num = send(sockfd,str,sizeof(str),0)) == -1) {
printf("send() error\n");
break;
}
if((num = recv(sockfd,buf,MAXDATASIZE,0)) == -1) {
printf("recv() error\n");
break;
}
buf[num-1]='\0';
printf("%s\n", buf);
sleep(3);
}
// 关闭套接字
close(sockfd);
return 0;
}
Makefile文件:
all: client server
client: client.c
gcc -o client client.c
server: server.c
gcc -o server server.c
clean:
rm -rf client server
编译运行后的结果,同一个主机通信:

如果服务端和客户端都在本地,可以使用共享 socket 文件的方式通信,而不是通过绑定端口。
不同主机通信:


四、总结
上面更多的是从应用的角度讲解了如果通过 socket 进行网络通信,后面有时间将会从更深层次的角度讲解。
五、参考文献
[1] https://www.cnblogs.com/langren1992/p/5101380.html
[2] https://blog.csdn.net/tanzongbiao/article/details/82344074
[3] https://blog.csdn.net/wjy741223284/article/details/98513237