一、套接字(Socket)
套接字是进行网络编程时非常重要的概念,它是一种通信的机制,可以在不同主机之间进行通信。套接字在网络编程中扮演着重要的角色,下面我们来一步步地了解它。
1.创建套接字
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
socket()函数可以创建一个新的套接字。其中,domain参数指定了通信协议的领域,如AF_INET表示IPv4协议族,AF_INET6表示IPv6协议族。type参数指定了套接字的类型,如SOCK_STREAM表示TCP类型。protocol参数指定使用的协议,一般为0。
2.绑定套接字
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr \*addr, socklen_t addrlen);
bind()函数将本地的Socket地址和创建的Socket进行绑定。其中,sockfd参数即为socket()函数返回的文件描述符。addr参数是一个指向本地Socket地址结构体的指针。addrlen参数是该结构体的长度。
3.监听套接字
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
listen()函数将sockfd指定的Socket转换成被动式的Socket,backlog指定了连接请求队列的最大长度。
4.接收连接请求
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr \*addr, socklen_t \*addrlen);
accept()函数从连接请求队列中取得一个连接请求,生成一个全新的Socket进行通信。其中,sockfd为listen()函数创建的Socket。addr和addrlen参数表示请求连接的客户端的地址信息。
二、进程和线程
进程和线程是操作系统中非常基础的概念,本篇文章着重介绍在网络编程中如何使用它们。
1.进程
进程是操作系统中进行资源分配和调度的基本单位。在网络编程中,我们可以使用fork()函数创建一个新进程,让进程分别运行不同的代码。
#include <unistd.h>
pid_t fork(void);
fork()函数会创建一个新的进程,并将父进程的数据复制到子进程中。父子进程是并发的,但是它们运行的是同一个代码。我们可以通过进程间的通信(IPC),使它们在运行过程中进行信息的交互。
2.线程
线程是进程中的更小单位,它可以轻松的与其他线程共享进程数据,便于实现并发。在Linux系统中,线程的实现依赖于POSIX的线程库。
#include <pthread.h>
int pthread_create(pthread_t \*thread, const pthread_attr_t \*attr, void *(*start_routine) (void *), void \*arg);
pthread_create()函数可以创建一个新的线程,其中参数thread是线程的标识符,attr指向线程属性的结构,start_routine是线程的入口地址,arg参数将传递给入口函数。
三、并发编程
在网络编程中,进程和线程是实现并发编程的基本手段。当多个客户端同时连接服务器时,我们需要一种方法来同时处理这些连接请求,这时候并发编程便派上了用场。
1.多进程实现并发
#include <sys/wait.h>
pid_t pid;
switch (pid = fork()) {
case -1:
perror("fork");
break;
case 0:
/* child */
exit(0);
break;
default:
/* parent */
waitpid(pid, NULL, 0);
break;
上述代码中,我们可以在主进程中调用fork()函数创建一个子进程来处理一个请求。这样,就可以同时处理多个请求。通过使用进程的基于文件描述符的机制可以实现进程之间的通信。
2.多线程实现并发
#include <pthread.h>
int threads_num;
pthread_t threads[THREAD_NUM];
/* create threads */
for (int i = 0; i < threads_num; i++) {
if (pthread_create(&threads[i], NULL, worker, NULL)) {
perror("pthread_create failed");
exit(1);
}
}
/* join threads */
for (int i = 0; i < threads_num; i++) {
if (pthread_join(threads[i], NULL)) {
perror("pthread_join failed");
exit(1);
}
}
上述代码中,我们使用pthread_create()函数创建若干个线程,并通过pthread_join()函数等待线程结束。线程可以共享数据,且线程和线程之间的切换速度比进程之间的切换速度要快得多。
四、网络编程实践
下面我们通过一个简单的例子来演示Linux网络编程的过程。
1.创建Socket
#include <sys/types.h>
#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket error");
exit(1);
}
上述代码中,我们使用socket()函数创建一个新的Socket。其中,参数AF_INET表示使用IPv4协议族,SOCK_STREAM表示使用TCP协议。
2.绑定Socket
#include <sys/types.h>
#include <sys/socket.h>
struct sockaddr_in server_addr;
bzero(\&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htons(INADDR_ANY);
server_addr.sin_port = htons(port);
int ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (ret == -1) {
perror("bind error");
exit(1);
}
上述代码中,我们使用bind()函数将Socket与本地的IP地址和端口进行绑定。
3.监听Socket
#include <sys/types.h>
#include <sys/socket.h>
int ret = listen(sockfd, backlog);
if (ret == -1) {
perror("listen error");
exit(1);
}
上述代码中,我们使用listen()函数将Socket转化为可接受的请求,backlog参数指定了连接请求队列的长度。
4.接受连接请求
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int connfd = accept(sockfd, (struct sockaddr *)&client_addr, &len);
if (connfd == -1) {
perror("accept error");
exit(1);
}
printf("accept from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
上述代码中,我们使用accept()函数从连接请求队列中取出一个连接请求生成一个全新的Socket,客户端的地址信息保存在client_addr结构体中,我们可以通过inet_ntoa()和ntohs()函数将网络字节顺序的IP地址和端口转换为可读形式。
5.接收和发送消息
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
上述代码中,我们使用recv()和send()函数进行接收和发送消息,分别对应于TCP中的read()和write()函数。
结论
以上便是Linux网络编程的详解,我们介绍了套接字、进程和线程、并发编程、以及通过一个简单例子演示了网络编程的基本流程。在进一步的使用和探索中,相信本篇文章为读者提供了很好的指引。