Linux操作系统 - 网络编程socket(2)

目录

服务器端

初始化

服务器启动

测试服务器

客户端

代码改进

1、多进程版本

2、多线程版本

三种版本的比较


之前有讲过基于UDP的网络编程一些基础的知识,现在看看基于TCP的网络编程。

首先TCP与UDP最大的不同是TCP是面向连接的,可靠传输。所以在编程实现方面有很多不同的地方,接下来看看具体的细节。

服务器端

在初始化的过程中,与UDP的方式有很多相同的地方,例如创建套接字,绑定端口号。而在TCP除了上述的操作以外还需要进行监听,即把套接字变成监听套接字。

初始化

class tcpServer
{
  private:
    int _port;
    int _lsock;
  public:
    tcpServer(int port = 8080)
      :_port(port)
    {}
    //初始化
    void initServer()
    {
      _lsock = socket(AF_INET,SOCK_STREAM,0);//创建套接字
      if(_lsock < 0)
      {
        cerr<<"sock error!"<

listen 函数

Linux操作系统 - 网络编程socket(2)_第1张图片

listen函数将套接字变为监听套接字,第一个参数就是传递创建好的套接字,第二个参数使用来确定最大接收连接的数量,这个地方涉及到全连接队列的知识,在后面说TCP协议的细节的时候在说一下这个参数,本身的目的是让系统效率最大化,不过这个参数不能太大。

服务器启动

当服务器启动之后需要接收来自客户端的连接。

accept函数

Linux操作系统 - 网络编程socket(2)_第2张图片

第一个参数是监听套接字(listen过后的套接字),第二个参数、第三个参数是输出型参数。

返回值

注意:返回值是一个文件描述符,也是一个socket,很关键,这个文件描述符就是实际通信过程中所需的文件描述符。

如果队列上不存在挂起的连接,并且套接字未标记为非阻塞,则accept()会阻塞调用方,直到存在连接为止。如果套接字标记为非阻塞,并且队列中不存在挂起的连接,则accept()将失败,并出现错误EAGAIN或EWOLDBLOCK。

void start()
{
  sockaddr_in remote;
  socklen_t len = sizeof(remote);
  while(true)
  {
    int sock = accept(_lsock,(struct sockaddr*)&remote,&len);
    if(sock < 0)
    {
      cerr<<"accept error"<

当获得到连接之后,需要对连接进行处理,自定义一下处理动作

void service(int sock)
{
  char buf[1024];
  while(true)                                                 
  {
    ssize_t s = recv(sock,buf,sizeof(buf)-1,0);
    if(s > 0)
    {
      buf[s] = '\0';
      cout<

其中有两个函数,一个recv函数,一个是send函数。

recv函数

Linux操作系统 - 网络编程socket(2)_第3张图片

在UDP里面讲过一个recvfrom函数,recvfrom函数针对UDP,需要告知是谁发过来的,有一个输出型的参数,而recv没有,因为TCP在接收连接(accept)的时候已经知道是谁发过来的,所以可以不用recvfrom函数。

参数分别是套接字、接收数据buf、期望接收的大小和阻塞接收标志flag。flag=0表示阻塞。

返回值

返回值为实际读到多少字节的数据,读到0表示对方已经断开连接,读到-1表示出错。

send函数

Linux操作系统 - 网络编程socket(2)_第4张图片

和sendto是一类接口,不过send是面向TCP的,不需要指名发给谁,因为TCP已经把连接建立好了,文件描述符在底层绑定了对方的ip地址和端口号。

可以发现这一批接口和系统IO中的read和write很相似,此时这两组接口都是面向字节流的(TCP就是面向字节流的),也就是说我们用read和write也可以直接读取套接字里面的数据或者往套接字里面写入数据。

int main(int argc,char* argv[])
{
  if(argc != 2)
  {
    cerr<<"parameter error!"<initServer();
  us->start();
  return 0;
}

测试服务器

运行一下

 

Linux操作系统 - 网络编程socket(2)_第5张图片

 

补充一个工具telnet

telnet是远程终端协议,是TCP/IP协议家族的成员之一,默认端口23。用来测试网络。

用telnet工具来作为客户端测试一下服务器。

首先需要安装一下:sudo yum install telnet telnet-server

 Linux操作系统 - 网络编程socket(2)_第6张图片

退出就是在telnet命令模式下ctrl ]  输入quit

Linux操作系统 - 网络编程socket(2)_第7张图片

 

客户端

与UDP不同的是,客户端需要发起连接

connect函数

Linux操作系统 - 网络编程socket(2)_第8张图片

第一个参数是文件描述符,第二个第三个参数想必已经不陌生了,需要对方的ip地址和端口号信息,以结构体的形式传递参数。

class tcpClient
{                                                                        
  private:
    string _ip;
    int _port;
    int sock;
  public:
    tcpClient(string ip = "127.0.0.1",int port = 8080)
      :_ip(ip)
      ,_port(port)
    {}
    void initClient()
    {
      sock = socket(AF_INET,SOCK_STREAM,0);//SOCK_STREAM面向字节流
      if(sock < 0)
      {
        cerr<<"sock error!"<

初始化完成后进行通信,这里就比较简单,自定义函数。

void start()
{
  while(true)
  {
    string msg;
    cout<<"please enter msg# ";
    cin>>msg;
    send(sock,msg.c_str(),msg.size(),0);
    char echo[128] = {'\0'};
    ssize_t s = recv(sock,echo,sizeof(echo)-1,0);
    if(s > 0)
    {
      echo[s] = '\0';
      cout<<"get msg from Server: "<

主函数

int main(int argc,char* argv[])
{
  if(argc != 3)
  {
    cerr<<"parameter error!"<initClient();
  uc->start();
  return 0;
}

运行结果 

Linux操作系统 - 网络编程socket(2)_第9张图片

 

上述程序面临的一些问题

面对多个请求如何实现?

代码改进

只针对服务器

1、多进程版本

修改一下start函数

void start()
{
  sockaddr_in remote;
  socklen_t len = sizeof(remote);
  while(true)
  {
    int sock = accept(_lsock,(struct sockaddr*)&remote,&len);
    if(sock < 0)
    {
      cerr<<"accept error\n"<

这里有很多细节

1、由于子进程会按照父进程的模板来创建,所以文件描述符资源也对应相同,子进程需要关闭一些自己并不关心的文件描述符资源,父进程也是如此。

2、子进程的退出,子进程的资源需要回收,此时需要父进程来处理(注意,不是父进程回收,父进程只是发起回收这个动作,实质上是内核完成资源的回收)。一般来说父进程需要等待(wait/waitpid)。还有一些其他的方法,比如说自定义捕捉(针对SIGCHLD信号),或者直接忽略,将资源交给内核回收。

忽略比较简单,在初始化那里加一行代码 signal(SIGCHLD,SIG_IGN);

测试:

Linux操作系统 - 网络编程socket(2)_第10张图片

同时有三个连接请求,将前两个进程放在后台

Linux操作系统 - 网络编程socket(2)_第11张图片

查看当前进程信息,三个客户端Client进程,三个子进程Server在处理连接,一个父进程Server。

这样服务器就可以同时应对多个连接。

但是多进程的方式资源消耗比较大,且进程间的切换开销也很大。引入多线程版本。

2、多线程版本

static void* service_routine(void* arg)
{
  pthread_detach(pthread_self());//分离线程,避免主线程阻塞等待释放资源
  cout<<"creat thread successfully,tid is "<

需要注意的几个点

1、由于在类里面,线程处理的函数得是静态函数,因为函数参数这里有一个隐藏的this指针,所以可以处理成静态函数的方式舍弃this指针,由于静态函数没有this指针,所以也无法调用类里面的service函数,此时也要把service设置为静态函数。

2、线程退出时也需要主线程释放资源,如果用pthread_join函数去释放资源,主线程会陷入阻塞状态,在上一个线程为退出的状态下,无法接收新的连接,所以可以采用分离线程的方式。

三种版本的比较

1、单进程:一般不使用

2、多进程版本:健壮性强,比较吃资源,效率低下

3、多线程版本:健壮性不强,较吃资源,效率相对较高

当大量客户端需要接入时,系统会存在大量的执行流。此时切换是影响效率的重要原因

你可能感兴趣的:(linux,linux,进程,socket,网络通信,线程)