Linux Socket 网络编程

2023-07-29,,

Linux下的网络编程指的是socket套接字编程,入门比较简单。在学校里学过一些皮毛,平时就是自学玩,没有见识过真正的socket编程大程序,比较遗憾。总感觉每次看的时候都有收获,但是每次看完了之后,过段时间不看,重新拾起这些知识的时候又要从头开始,所以,在这里做个笔记也算是做个模板,以后可以直接从某一个阶段开始接着玩。。。

1. socket套接字介绍

socket机制其实就是包括socket, bind, listen, connect, accept等函数的方法,其通过指定的函数实现不同的协议(IP4,IP6等)的数据在不同层之间的传输和获取等处理。其实个人理解socket就是处于应用层和TCP/IP协议之间的一个中间层,具体的数据分析,重组,拆分等操作对于应用层的网络编程者来说都是不可见的,这些都有协议栈内核实现,应用层的网络编程会通过设置socket机制中创建socket时参数不同,而接收或者发送不同类型的数据。

对于TCP/IP在这里就不过多的讲,但是需要提及的是经典的TCP/IP参考模型是分为4个层次:应用层,传输层,网络互联层,主机到网络层。标准的套接字编程主要是指TCP和UDP的网络编程,socket网络编程的模式就是分server和client,通过server端首先建立,client端联接进行通信。网络协议栈内核实现的功能主要就是在数据到达每一层时,给数据加上或者去掉协议包头,或者进行校验,数据重组,拆分等操作,最后得到我们想要的数据格式。

下面简单列一下TCP/IP参考模型中主要的协议类型(图片来自Linux网络编程)。

图1 TCP/IP 参考模型的层次结构

标准套接字分为TCP和UDP协议两种不同type的工作流程,TCP网络编程相对于UDP来说相对复杂,因为TCP是面向连接的服务,其中包括三次握手建立连接的过程,而UDP则是无连接的服务,下图介绍了TCP服务使用socket套接字建立连接的过程,以及进行数据交互的过程。图片来自http://blog.csdn.net/xrb66/article/details/6048399

图2 TCP 建立socket通信的流程

TCP和UDP的网络编程模式有两种,一种是服务器模式,另一种是客户端模式,因为TCP是面向连接的服务,所以在socket机制当中,TCP的服务器模式比UDP的服务器模式多了listen,accept函数,TCP客户端比UDP客户端多了connect函数。下面是TCP和UDP网络编程的两种模式流程图。下面将结合图2,3,4介绍一下TCP socket的机制是如何实现的。

图3 TCP 服务器端与客户端通信流程

图4 UDP服务器端和客户端通信流程

2. socket套接字基本函数介绍

2.1 创建socket套接字

int socket(int family, int type, int protocol);

功能介绍:

在Linux操作系统中,一切皆文件,这个大家都知道,个人理解创建socket的过程其实就是一个获得文件描述符的过程,当然这个过程会是比较复杂的。可以从内核中找到创建socket的代码,并且socket的创建和其他的listen,bind等操作分离开来。socket函数完成正确的操作是返回值大于0的文件描述符,当返回小于0的值时,操作错误。同样是返回一个文件描述符,但是会因为三个参数组合不同,对于数据具体的工作流程不同,对于应用层编程来说,这些也是不可见的。

参数说明:

从socket创建的函数可以看出,socket有三个参数,family代表一个协议族,比较熟知的就是AF_INET,PF_PACKET等;第二个参数是协议类型,常见类型是SOCK_STREAM, SOCK_DGRAM, SOCK_RAW, SOCK_PACKET等;第三个参数是具体的协议,对于标准套接字来说,其值是0,对于原始套接字来说就是具体的协议值。

2.2 地址端口绑定函数bind

int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

功能介绍:

bind函数主要应用于服务器模式一端,其主要的功能是将addrlen长度 struct sockaddr类型的myaddr地址与sockfd文件描述符绑定到一起,在sockaddr中主要包含服务器端的协议族类型,网络地址和端口号等。在客户端模式中不需要使用bind函数。当bind函数返回0时,为正确绑定,返回-1,则为绑定失败。

参数说明:

bind函数的第一个参数sockfd是在创建socket套接字时返回的文件描述符。

bind函数的第二个参数是struct sockaddr类型的数据结构,由于struct sockaddr数据结构类型不方便设置,所以通常会通过对truct sockaddr_in进行地质结构设置,然后进行强制类型转换成struct sockaddr类型的数据,下面是两种类型数据结构的定义和对应关系图。

typedef unsigned short  sa_family_t;
struct in_addr {
__be32 s_addr;
}; struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[]; /* 14 bytes of protocol address */
}; /* Structure describing an Internet (IP) socket address. */
#define __SOCK_SIZE__ 16 /* sizeof(struct sockaddr) */
struct sockaddr_in {
sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */ /* Pad to size of `struct sockaddr'.                  */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};

图5 struct sockaddr_in和struct sockaddr的映射关系

bind函数的第三个参数是指定struct sockaddr类型数据的长度,因为前面讲过bind函数的第二个参数是通过设置一个较容易的数据结构,然后通过强制类型转换成struct sockaddr,实际上,第二个参数具体的数据结构的长度会根据socket创建时,设置的family协议族的不同而不同,像AF_UNIX协议族的bind函数第二个参数的数据结构应该是struct sockaddr_un,其大小和struct sockaddr_in不同。

2.3 监听本地端口listen

int listen(int sockfd, int backlog);

功能介绍:

刚开始理解listen函数会有一个误区,就是认为其操作是在等在一个新的connect的到来,其实不是这样的,真正等待connect的是accept操作,listen的操作就是当有较多的client发起connect时,server端不能及时的处理已经建立的连接,这时就会将connect连接放在等待队列中缓存起来。这个等待队列的长度有listen中的backlog参数来设定。listen和accept函数是服务器模式特有的函数,客户端不需要这个函数。当listen运行成功时,返回0;运行失败时,返回值位-1.

参数说明:

sockfd是前面socket创建的文件描述符;backlog是指server端可以缓存连接的最大个数,也就是等待队列的长度。

2.4 接受网络请求函数accept

int accept(int sockfd, struct sockaddr *client_addr, socklen_t *len);

功能介绍:

接受函数accept其实并不是真正的接受,而是客户端向服务器端监听端口发起的连接。对于TCP来说,accept从阻塞状态返回的时候,已经完成了三次握手的操作。Accept其实是取了一个已经处于connected状态的连接,然后把对方的协议族,网络地址以及端口都存在了client_addr中,返回一个用于操作的新的文件描述符,该文件描述符表示客户端与服务器端的连接,通过对该文件描述符操作,可以向client端发送和接收数据。同时之前socket创建的sockfd,则继续监听有没有新的连接到达本地端口。返回大于0的文件描述符则表示accept成功,否则失败。

参数说明:

sockfd是socket创建的文件描述符;client_addr是本地服务器端的一个struct sockaddr类型的变量,用于存放新连接的协议族,网络地址以及端口号等;第三个参数len是第二个参数所指内容的长度,对于TCP来说其值可以用sizeof(struct sockaddr_in)来计算大小,说要说明的是accept的第三个参数要是指针的形式,因为这个值是要传给协议栈使用的。

2.5 连接目标服务器函数connect

int connect(int sock_fd, struct sockaddr *serv_addr,int addrlen);

功能介绍:

连接函数connect是属于client端的操作函数,其目的是向服务器端发送连接请求,这也是从客户端发起TCP三次握手请求的开始,服务器端的协议族,网络地址以及端口都会填充到connect函数的serv_addr地址当中。当connect返回0时说明已经connect成功,返回值是-1时,表示connect失败。

参数说明:

connect的第一个参数是socket创建的文件描述符;第二个参数是一个struct sockaddr类型的指针,这个参数中设置的是要连接的目标服务器的协议族,网络地址以及端口号;第三个参数表示第二个参数内容的大小,与accept不同,这个值不是一个指针。

在服务器端和客户端建立连接之后是进行数据间的发送和接收,主要使用的接收函数是recv和read,发送函数是send和write。因为对于socket套接字来说,最终实际操作的是文件描述符,所以可以使用对文件进行操作的接收和发送函数对socket套接字进行操作。对于UDP编程来说,其服务器端和客户端之间没有三次握手建立连接,所以服务器端没有listen和accept函数,客户端没有connect函数。所以对于服务器端来说,没有accept函数,所以使用recvfrom函数来获取数据的同时获得客户端的协议族,网络地址以及端口号;对于客户端来说,没有connect函数,所以使用sendto函数发送数据的同时设置服务器端的协议族,网络地址以及端口;同理如果recvfrom用在客户端,则是接收服务器端数据和地址,sendto用在服务器端,则是发送到客户端网络地址以及端口数据。

2.6 接收数据函数recvfrom

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);

功能介绍:

对于该函数主要的功能是,从客户端或者服务器端接收数据以及发送方的地址信息存储到本地的struct sockaddr类型参数变量当中,如果函数返回-1,所说明接收数据失败,如果返回的是大于等于0的值,则说明函数接收到的数据的大小。因为可以设置文件描述符的状态为阻塞模式,所以在没有接收到数据时,recvfrom会一直处于阻塞状态,直到有数据接收到。

参数说明:

sockfd是创建socket时的文件描述符;buf用于存储接收到的数据缓冲区,接收的数据将放到这个指针所指向的内容的空间中;len是接收缓冲区的大小;from是指向struct sockaddr的指针,接收发送方地址信息;fromlen是表示第5个参数所指向内容的长度,可以使用sizeof(struct sockaddr)来定义大小,不过因为是要传给内核协议栈,所以使用了指针类型。

2.7 发送数据函数sendto

sizeof_t sendto(int sockfd, const void *buf, size_t len, int flag, const struct sockaddr *to, socklen_t tolen);

功能介绍:

sendto函数主要根据填充的接收方的地址信息向客户端或者服务器端发送数据,接收方的地址信息会提前设置在struct sockaddr类型的参数指针中,当返回值-1时,表明发送失败,当返回值大于等于0时,表示发送成功,并且发送数据的大小会通过返回值传递回来。

参数说明:

sockfd是有socket创建的文件描述符;buf是发送数据缓冲区,要发送的数据会放在这个指针指向的内容空间中;len是发送缓冲区的大小;to是一个struct sockaddr类型的指针,其指向地址的内容是接收方地址信息;tolen表示第5个参数指向的数据内容的长度,传递的是值,可以用sizeof(struct sockaddr)计算。

3. linux线程介绍

通过socket机制建立起的连接,仅仅实现的是服务器端和客户端之间的通信,数据的传输。但是要使网络编程实现性能更优的话,少不了使用线程,线程间通信以及IO函数,接下来就简单讲一下线程,线程间通信,以及IO函数中的select函数。

Linux下的线程,线程是进程中的一个运行单元,进程fork子进程的过程是对父进程进程copy的过程,然后紧紧改变子进程本身的一些变量,之后各自的进程运行属于自己进程空间的内容;而线程的创建则不然,线程创建在进程中有自己固定的创建函数,在同一个进程中创建的所有线程会共用所在进程的全局变量,信号句柄,文件描述符和当前的目录状态,但是每个线程又会有属于自己的线程栈等私有的属性。进程获得的使用资源被分给了每个线程,除公共部分外每个线程之间的运行又是相对独立的。

Linux下线程的基本函数:

3.1 线程创建函数pthread_create

int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

功能介绍:

该函数是用于在进程中创建线程,线程在进程中创建有固定的形式。个人理解,线程的创建就是圈起了一段代码作为一个线程,这段被圈起来的函数作为线程函数,线程开始运行就是从线程函数开始运行,线程函数也有固定的格式,因为格式固定,线程的创建把单独作为参数的线程函数和线程函数参数整合到一起,形成一个线程。当然在创建的同时,会设置当前线程的属性,以及用于操作的线程标识符。

参数说明:

thread:第一个参数是一个pthread_t类型的线程标识符,可以通过操作该标识符,实现对线程的操作;

attr:第二个参数是用来设置线程的属性,包括线程优先级等属性;

start_routine:第三个参数是指当线程成功创建后,开始运行的一个单元,该单元需要自己编写,一般会使用无限循环来实现;

arg:第四个参数是第三个参数线程函数运行时传入的参数,为了防止每个线程函数输入参数不同而难以操作,所以线程创建讲两者分开,这样更灵活,便于操作。

3.2 线程结束函数pthread_join和pthread_exit

3.2.1 线程函数结束pthread_exit

void pthread_exit(void *retval);

功能介绍:

该函数主要的功能是从被圈起来的线程函数中退出,退出过程中会通过函数的参数指针带出一个对象,当等待线程结束函数pthread_join的第二个参数不是NULL时,会传给这个参数做相应的处理。

参数说明:

函数的参数是一个指针,通过该指针可以传递出当前进程结束时的相关信息,这个值会被pthread_join捕捉到。

3.2.2 等待线程结束pthread_join

int pthread_join(pthread_t th,void **return_value);

功能介绍:

函数主要功能是等待线程结束,pthread_exit是主动结束线程,该函数是被动等待线程结束。函数会处于等待状态,如果函数的第二个参数没有设置为NULL,则会捕捉到从exit传递回的信息。

参数说明:

第一个参数是要等待的线程的标识符,有phread_create函数第一个参数指定其值是多少;第二个参数是一个二维指针,用于等待从pthread_exit返回值。当然,如果不适用pthread_exit结束线程函数的话,线程函数结束,也就是调用函数的线程结束。当线程函数运行结束时,该函数用于回收线程的资源。

对于讲理论来说,大家往往更喜欢实例,下面是一个线程的小例子。

 #include <stdio.h>
#include <string.h>
#include <pthread.h> pthread_t pth[]; void *print_message(void *argv)
{
printf("This is in thread %x!\n", *((pthread_t *)argv)); if(&pth[] == argv)
{
sleep();
pthread_exit("1 thread exit!");
}
else
{
sleep();
pthread_exit("2 thread exit!");
}
} int main(void)
{
void *returnValue; printf("This is in main function BEFORE pthread create!\n");
pthread_create(&pth[], NULL, &print_message, &pth[]);
pthread_create(&pth[], NULL, &print_message, &pth[]);
printf("This is in main function AFTER pthread create %x!\n", pth[]);
pthread_join(pth[], &returnValue);
printf("This is in main function AFTER pthread join 1\n");
pthread_join(pth[], &returnValue);
printf("This is in main function returnValue=%s\n", returnValue);
printf("This is in main function AFTER pthread join\n");
return ;
}

下面是运行结果:

这个只是运行结果,其实一些动态的东西,也看不到,因为pthread_join是阻塞等待线程结束的,所以说这个代码是线程1等待一秒首先结束线程运行,pthread_join会捕捉到线程结束,线程2会在线程1结束运行后约9s然后结束运行,这时候线程2的pthread_join才会捕捉到线程结束并释放资源。所以如果是使用多线程并且在同一个地方统一使用pthread_join释放资源时,最好先释放首先结束运行的线程,然后在释放后结束的线程。否则的话如果使用pthread_join先释放后结束运行的线程,先结束运行的线程资源因为等待前面pthread_join结束而得不到释放。

因为线程是在进程中创建的,线程公用进程中的资源,所以线程资源的释放非常重要。主要的线程资源释放的方法有下面三种:

1) 在线程函数中调用pthread_detach(pthread_self()),主动释放线程资源;

2) 向上面介绍的pthread_join函数,被动释放线程资源;

3) 通过设置线程属性中的__detachstate属性,在线程函数运行完,或者pthread_exit退出时,自动释放线程资源,设置线程属性通过下面方式,

pthread_attr_t att; //线程属性
pthread_attr_init(&att);//初始化线程属性
pthread_attr_setdetachstate(&att, PTHREAD_CREATE_DETACHED);//设置线程属性
pthread_create( pthread_t *pthread, &att, void *(*thread_function)(void *), (void *argv)); //建立线程

3.3 线程的属性

线程创建函数pthread_create的第二个参数是指线程的属性,当该参数设置为NULL时,表示使用了线程的默认属性。其实我们可以通过设置第二个参数来设置线程的属性。线程属性的改变有属于自己的结构体和函数。

线程属性的结构体

typedef struct __pthread_attr_s
{
int __detachstate; /*线程的终止状态*/
int __schedpolicy; /*调度优先级*/
int __sched_param __shedparam; /*参数*/
int __inheritsched; /*继承*/
int __scope; /*范围*/
int __guardsize; /*保证尺寸*/
int __stackaddr_set /*运行栈*/
void *__stackaddr; /*线程运行栈地址*/
size_t __stacksize; /*线程运行栈大小*/
}pthread_attr_t;

线程主要的属性对象包括上面提到的这几种,线程的属性不可以直接设置,需要通过特定的函数来实现,可以通过函数对上面这几种线程属性进行修改。并且线程属性的修改要在创建线程之前完成。线程属性的初始化函数为pthread_attr_init,通过如下所示的函数可以说明线程的属性参数都可以做修改,需要再次强调的是这些参数的修改需要在创建线程之前完成,将修改后的属性参数通过pthead_create的第二个参数传入到线程中,具体函数如下:(具体如何使用可参考http://blog.csdn.net/hudashi/article/details/7709413)

1). 线程优先级设置属性,需要先获得线程当前属性优先级,然后通过设置操作修改属性优先级

int pthread_attr_getschedparam (pthread_attr_t* attr, struct sched_param* param);

int pthread_attr_setschedparam (pthread_attr_t* attr, struct sched_param* param);

2). 设置线程范围属性

int pthread_attr_setscope (pthread_attr_t* attr, int scope);

3). 设置线程终止状态属性

int pthread_attr_setdetachstate (pthread_attr_t* attr, int detachstate);

4). 设置线程保护区大小属性

int pthread_attr_setguardsize(pthread_attr_t* attr,size_t guardsize);

5). 设置线程继承调度属性
int pthread_attr_setinheritsched(pthread_attr_t* attr, int inheritsched);

6). 设置线程栈基址以及堆栈的最小尺寸大小
int pthread_attr_setstack(pthread_attr_t* attr, void* stackader,size_t stacksize);

7). 设置线程栈基址属性
int pthread_attr_setstackaddr(pthread_attr_t* attr, void* stackader);

8). 设置线程栈的大小属性
int pthread_attr_setstacksize(pthread_attr_t* attr, size_t stacksize);

4. 线程间通信之信号量

线程的信号量主要就是实现对公共资源的一种控制管理。当公共资源增加时,信号量的值增加;当公共资源减少时,信号量的值减少;只有信号量的值大于0时,才能访问信号量所代表的公共资源。其实功能和之前的ucos的信号量功能类似。

4.1 线程信号量初始化sem_init

int sem_init(sem_t *sem, int pshared, unsigned int value);

功能介绍:

该函数主要的功能是创建一个信号量,设置该信号量的值,并且设置信号量的使用范围。信号量创建成功后,可以对其进行加减操作。

参数说明:

第一个参数sem是一个指向信号量结构的指针,当信号量初始化成功后,可以的这个信号量指针进行加减操作;第二个参数表示信号量的共享属性,当其值不为0时,信号量可以在进程间共享,如果等于0,则只能在同一个进程中的多个线程间共享;第三个参数用于设置信号量初始化时候的值。

4.2 线程信号量增加函数sem_post

int sem_post(sem_t *sem);

功能介绍:

该函数用于增加信号量的值,每次增加值为1。当有线程在等待该信号量,则等待信号量返回,不增加信号量的值。

参数说明:

sem参数是初始化时候创建的信号量结构体,用于记录信号量值得参数。

4.3 线程信号量减少函数sem_wait

int sem_wait(sem_t *sem);

功能介绍:

该函数用于减少信号量的值,每次减少值为1。当信号量的值为0,则线程会阻塞一直等待信号量的值大于0为止,当值为0时,不在减少。

参数说明:

sem参数是初始化时候创建的信号量结构体。

4.4 线程信号量的销毁函数sem_destroy

int sem_destroy(sem_t *sem);

功能介绍:

函数用于释放创建的信号量。

参数说明:

sem参数是初始化时候创建的信号量结构体。

使用线程的信号量进行通信,可以有效的对线程资源进行合理的分配,同时可以使线程以一个合理的方式进行调度。下面的代码是一个小小的实例,用很常规的方法来使用信号量,当线程获得获得信号量之后,在处理完相应的操作之后会主动的释放掉信号量。其实可以根据信号量的wait和post自己设计使用信号量。不过需要注意的是在wait和post函数分开使用时,可能会因为线程优先级等问题,post和wait被调用的次数不是对等的,这时候这时候可能出现的问题是不可控的,在这种方式设计程序时,需要把大部分的情况考虑进去,当然可能出现的情况的种数也是从0到1质变,从1到n量变得过程。

 #include <stdio.h>
#include <string.h>
#include <semaphore.h>
#include <pthread.h> sem_t sem;
int semValue; void *pthread_wait(void *argv)
{
while()
{
sem_wait(&sem);
sem_getvalue(&sem, &semValue);
printf("This is in phtread WAIT function! sem = %d\n", semValue);
sem_post(&sem);
sleep();
}
pthread_exit("exit wait pthread!\n");
} void *pthread_post(void *argv)
{
while()
{
sem_wait(&sem);
sem_getvalue(&sem, &semValue);
printf("This is in phtread POST function! sem = %d\n", semValue);
sem_post(&sem);
sleep();
}
pthread_exit("exit post pthread!");
} int main(void)
{
pthread_t pt[];
void *ret; sem_init(&sem,,);
pthread_create(&pt[], NULL, &pthread_wait, NULL);
pthread_create(&pt[], NULL, &pthread_post, NULL);
pthread_join(pt[], &ret);
printf("return value %s", ret);
pthread_join(pt[], &ret);
printf("return value %s", ret); return ;
}

上面代码的运行结果如下,因为是线程采用了无限循环的方式,所以pthread_exit和pthread_join不会执行到。

如果将上面代码中的第26行注释掉,运行结果如下,可以看到信号量的值一直在增加,就如上面提到的因为sem_wait函数和sem_post函数不能同时使用时,会出现一些不可控的运行结果。也可以利用这种情况,设计程序。

5. 线程间通信之互斥锁

线程互斥锁主要的功能是在一段时间内,只允许一个线程对一段代码或者资源进行访问的机制。当有一个线程获得互斥锁后,其他线程如果想获得互斥锁就会被阻塞,直到占有互斥锁的线程释放互斥锁为止。线程的互斥主要包含的函数有如下几个:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_trylock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);

int pthread_mutex_destory(pthread_mutex_t *mutex);

功能介绍:

pthread_mutex_init函数主要的功能是初始化一个互斥锁,并且设置该互斥锁的属性;pthread_mutex_lock主要的功能是给互斥锁上锁,如果一个线程给互斥锁上锁之后,其他线程想获得该锁的使用权,必须要阻塞等待,直到占有互斥锁的线程释放该锁;函数是pthread_mutex_lock函数的非阻塞版本。如果mutex参数所指定的互斥锁已经被锁定的话,调用pthread_mutex_trylock函数不会阻塞当前线程,而是立即返回一个值来描述互斥锁的状况;pthread_mutex_unlock给互斥锁解锁,线程调用该函数之后会释放掉已经上锁的互斥锁;pthread_mutex_destory是init创建的互斥锁释放掉。

参数说明:

pthread_mutex_t是互斥锁机制内核私有的数据结构,用于实现互斥锁机制必不可少的元素。上述函数已经很好的诠释了如何使用该参数。

pthread_mutexattr_t是用来描述线程互斥锁属性的结构体,初始化时,如果属性参数设置为NULL的话,则表示使用默认设置。

个人感觉互斥锁就是信号量的值为1时候的一种特殊情况,只是给互斥锁加了一些限制,并且没有设置值而已。互斥锁中的lock相当于信号量中的wait,而unlock相当于post操作。获取互斥锁并且lock上锁之后,只能通过unlock解锁才可以重新获取互斥锁,互斥锁主要的功能就是对线程资源进行保护,同一个时间只有一个线程可以获得互斥锁,对资源进行使用,只有互斥锁被释放,其他的线程才可以重新获得互斥锁。不想信号量一样,可以通过设置信号量的值来实现多个线程可以同时操作使用线程资源。下面是一个互斥锁的实例。

 #include <stdio.h>
#include <string.h>
#include <pthread.h> pthread_mutex_t mutex;
int mutexCounter; void *pthread_mutex_LOCK(void *argv)
{
while()
{
pthread_mutex_lock(&mutex);
mutexCounter++;
printf("This is in phtread mutex LOCK function! Counter = %d\n", mutexCounter);
pthread_mutex_unlock(&mutex);
sleep();
}
pthread_exit("exit wait pthread!\n");
} void *pthread_mutex_UNLOCK(void *argv)
{
while()
{
pthread_mutex_lock(&mutex);
mutexCounter--;
printf("This is in phtread mutex UNLOCK function! Counter = %d\n", mutexCounter);
pthread_mutex_unlock(&mutex);
sleep();
}
pthread_exit("exit post pthread!");
} int main(void)
{
pthread_t pt[];
void *ret; pthread_mutex_init(&mutex, NULL);
pthread_create(&pt[], NULL, &pthread_mutex_LOCK, NULL);
pthread_create(&pt[], NULL, &pthread_mutex_UNLOCK, NULL);
pthread_join(pt[], &ret);
printf("return value %s", ret);
pthread_join(pt[], &ret);
printf("return value %s", ret); return ;
}

上面代码的运行结果如下所示,按逻辑来说应该是一个LOCK函数,一个UNLOCK函数交替运行,但实际的运行结果却是如下图所示,主要的原因是在运行完两个函数之后,都处于sleep等待状态,因为处理器运行速度太快,两个函数完成等待的时间相同,这时候线程的调度就不是按照原来的逻辑了。所以在线程函数处理内容较少时,要注意时序对对编程逻辑的影响。

6. IO复用之select函数

select函数用于IO复用,它用于监视多个文件描述符集合,看规定时间内有没有事件产生。

int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);

功能介绍:

该函数主要的功能是对需要操作的文件描述符集合进行查询,目标文件描述符中有可以读,写或者异常错误操作的情况时,会返回一个大于0的整数值,表示可以对该文件描述符进行操作。select函数返回0,表示超时;返回-1,表示发生错误;返回大于0的整数值,表示有符合要求的文件描述事件产生。当不需要监视某种文件描述符时,设置参数为NULL。

参数说明:

nfds:是一个整型变量,其值是加入到后面三个文件描述符集合中的最大文件描述符的值加1。

readfds:可读文件描述符集合,通过FD_SET向该文件描述符集合中加入需要监视的目标文件描述符,当有符合要求的文件描述符时,select会返回一个大于0的值,同时会把原来集合中的不可读的文件描述符清掉,如果想在次监视可读文件描述,需要重新FD_SET。

writefds:可写文件描述符集合,同样通过FD_SET函数向结合中加入需要被监视的目标文件描述符,select返回时,同样会把不可写文件描述符清掉,如果需要重新监视文件描述符,需要重新FD_SET设置。

exceptfds:该描述符集合是用于监视文件描述符集合中的任何文件是否发生错误。

timeout:用于设置超时的最长等待时间,如果在该规定时间内没有返回一个大于0的值,则返回0,表示超时。如果超时间设置为NULL,表示阻塞等待,直到符合条件的文件描述符在集合中出现,当timeout的值为0时,select会立即返回。

timeout的数据结构如下:

struct timeval
{
time_t tv_sec; /*秒*/
long tv_usec; /*微秒*/
};

有4个宏可以操作文件描述符集合:

FD_ZERO: 用于清空文件描述符集合,FD_ZERO(&fds)。

FD_SET:向某个文件描述符结合中加入文件描述符, FD_SET(fd, &fds)。

FD_CLR:从某个文件描述符结合中取出某个文件描述符, FD_CLR(fd, &fds)。

FD_ISSET:测试某个文件描述符是否在某个文件描述符集合中, FD_ISSET(fd, &fds)。

下面是本实用socket编程,并且利用select IO实现的一个server和client实时通信的例子,为了显示更直观,加了一些打印以及接收数据上的操作,程序还有bug,希望阅读的人不要介意,或者自行修改。代码如下,可以作为学习socket和select的一个实例。

server.c文件源码如下:

 #include <stdio.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <time.h> #define SPORT 8888
#define BACKLOG 5
#define SIZE 100 int main(void)
{
int sockfd, clientfd;
struct sockaddr_in sockServer, sockClient;
struct timeval tv;
fd_set readfds, writefds;
int readlen, writelen;
char buffer[SIZE]; sockfd = socket(AF_INET, SOCK_STREAM, );
if(sockfd < )
{
perror("create socket failed!\n");
return -;
} bzero(&sockServer, );
sockServer.sin_family = AF_INET;
sockServer.sin_port = htons(SPORT);
sockServer.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(sockfd, (struct sockaddr *)&sockServer, sizeof(struct sockaddr_in)) < )
{
perror("bind socket failed!\n");
return -;
} if(listen(sockfd, BACKLOG) < )
{
perror("listen failed!\n");
} printf("Server is listening ......\n"); while()
{
int len = sizeof(struct sockaddr_in);
int ret;
time_t timet; clientfd = accept(sockfd, (struct sockaddr *)&sockClient, &len);
if(clientfd < )
{
perror("accept failed!\n");
return -;
} for(;;)
{
FD_ZERO(&readfds);
FD_SET(, &readfds);
FD_SET(clientfd, &readfds);
tv.tv_usec = ;
tv.tv_sec = ; ret = select(clientfd+, &readfds, NULL, NULL, &tv);
switch(ret)
{
case :
printf("select timeout!\n");
break;
case -:
perror("select return failed!\n");
goto closesocket;
default:
if(FD_ISSET(clientfd, &readfds) > )
{
memset(buffer, , SIZE);
readlen = read(clientfd, buffer, SIZE);
if(readlen < )
{
perror("read data failed!\n");
goto closesocket;
}
time(&timet);
printf("Opposite: %d %s", clientfd, ctime(&timet));
strcat(buffer, "\n");
writelen = write(, buffer, readlen+);
if(writelen < )
{
perror("write data failed!\n");
goto closesocket;
}
}
if(FD_ISSET(, &readfds) > )
{
time(&timet);
printf("Owner: %d %s\n", sockfd, ctime(&timet));
memset(buffer, , SIZE);
readlen = read(, buffer, SIZE);
if(readlen < )
{
perror("read data failed!\n");
goto closesocket;
}
writelen = write(clientfd, buffer, readlen);
if(writelen < )
{
perror("write data failed!\n");
goto closesocket;
}
}
}
}
closesocket:
close(clientfd);
}
close(sockfd); return ;
}

client.c文件源码如下:

 #include <stdio.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <time.h> #define SPORT 8888
#define SIZE 100 int main(void)
{
int sockfd, clientfd;
struct sockaddr_in sockServer;
struct timeval tv;
fd_set readfds;
int readlen, writelen;
char buffer[SIZE];
time_t timet; sockfd = socket(AF_INET, SOCK_STREAM, );
if(sockfd < )
{
perror("create socket failed!\n");
return -;
} bzero(&sockServer, );
sockServer.sin_family = AF_INET;
sockServer.sin_port = htons(SPORT);
sockServer.sin_addr.s_addr = htonl(INADDR_ANY); if(connect(sockfd, (struct sockaddr *)&sockServer, sizeof(struct sockaddr_in)) < )
{
perror("connect failed!\n");
close(sockfd);
} while()
{
int ret; FD_ZERO(&readfds);
FD_SET(, &readfds);
FD_SET(sockfd, &readfds);
tv.tv_usec = ;
tv.tv_sec = ; ret = select(sockfd+, &readfds, NULL, NULL, &tv);
switch(ret)
{
case :
printf("select timeout!\n");
break;
case -:
perror("select return failed!\n");
goto closesocket;
default:
if(FD_ISSET(sockfd, &readfds) > )
{
memset(buffer, , SIZE);
readlen = read(sockfd, buffer, SIZE);
if(readlen < )
{
perror("read data failed!\n");
goto closesocket;
}
time(&timet);
printf("Opposite: %s %s", "Server", ctime(&timet));
strcat(buffer, "\n");
writelen = write(, buffer, readlen + );
if(writelen < )
{
perror("write data failed!\n");
goto closesocket;
}
}
if(FD_ISSET(, &readfds) > )
{
time(&timet);
printf("Owner: %d %s\n", sockfd, ctime(&timet));
memset(buffer, , SIZE);
readlen = read(, buffer, SIZE);
if(readlen < )
{
perror("read data failed!\n");
goto closesocket;
}
writelen = write(sockfd, buffer, readlen);
if(writelen < )
{
perror("write data failed!\n");
goto closesocket;
}
}
} closesocket:
close(clientfd);
}
close(sockfd); return ;
}

运行结果如下所示:

Linux Socket 网络编程的相关教程结束。

《Linux Socket 网络编程.doc》

下载本文的Word格式文档,以方便收藏与打印。